diff --git a/app/(app)/event/[naddr]/_components/Header.tsx b/app/(app)/event/[naddr]/_components/Header.tsx
new file mode 100644
index 0000000..55d1be5
--- /dev/null
+++ b/app/(app)/event/[naddr]/_components/Header.tsx
@@ -0,0 +1,290 @@
+"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 { HiOutlineLightningBolt } from "react-icons/hi";
+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";
+import { formatDate } from "@/lib/utils/dates";
+import SmallCalendarIcon from "@/components/EventIcons/DateIcon";
+import LocationIcon from "@/components/EventIcons/LocationIcon";
+
+const EditListModal = dynamic(() => import("@/components/Modals/EditList"), {
+ ssr: false,
+});
+const CreateListEvent = dynamic(
+ () => import("@/components/Modals/ShortTextNoteOnList"),
+ {
+ ssr: false,
+ },
+);
+const ConfirmModal = dynamic(() => import("@/components/Modals/Confirm"), {
+ 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 [syncingUsers, setSyncingUsers] = useState(false);
+ const { pubkey, tags } = event;
+ const { profile } = useProfile(pubkey);
+ console.log("EVENT", tags);
+
+ const noteIds = getTagsValues("e", tags).filter(Boolean);
+ const title = getTagValues("name", tags) ?? "Untitled";
+ console.log("tite", tags);
+ const image =
+ getTagValues("image", tags) ??
+ getTagValues("picture", tags) ??
+ getTagValues("banner", tags) ??
+ profile?.banner;
+
+ const description = event.content;
+ const startDate = getTagValues("start", tags)
+ ? new Date(parseInt(getTagValues("start", tags) as string) * 1000)
+ : null;
+ const endDate = getTagValues("end", tags)
+ ? new Date(parseInt(getTagValues("end", tags) as string) * 1000)
+ : null;
+ const getLocation = () => {
+ let temp = getTagAllValues("location", tags);
+ if (temp[0]) {
+ return temp;
+ }
+ return getTagAllValues("address", tags);
+ };
+ const location = getLocation();
+ const rawEvent = event.rawEvent();
+ const subscriptionsEnabled = !!getTagValues("subscriptions", rawEvent.tags);
+ const priceInBTC = parseFloat(getTagValues("price", rawEvent.tags) ?? "0");
+ const isMember =
+ currentUser &&
+ getTagsValues("p", rawEvent.tags).includes(currentUser.pubkey);
+
+ useEffect(() => {
+ if (!currentUser || !subscriptionsEnabled) 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 handleSyncUsers() {
+ if (!event || !ndk) return;
+ setSyncingUsers(true);
+ try {
+ console.log("handleSyncUsers");
+ await updateListUsersFromZaps(ndk, event.tagId(), rawEvent);
+ toast.success("Users Synced!");
+ } catch (err) {
+ console.log("error syncing users", err);
+ } finally {
+ setSyncingUsers(false);
+ }
+ }
+ async function handleSendZap() {
+ try {
+ const result = await sendZap(
+ ndk!,
+ btcToSats(priceInBTC),
+ rawEvent,
+ `Access payment: ${title}`,
+ );
+ toast.success("Payment Sent!");
+ void handleCheckPayment();
+ } catch (err) {
+ console.log("error sending zap", err);
+ } finally {
+ }
+ }
+ if (!event) {
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+ {!!image && (
+
+ )}
+
+
+
+
+
+
+ {!!currentUser && currentUser.pubkey === pubkey && (
+ <>
+
+
+
+ >
+ )}
+ {subscriptionsEnabled &&
+ !isMember &&
+ (hasValidPayment ? (
+
+ ) : (
+
+ ))}
+
+
+
+
+ {!!description && (
+
+ {description}
+
+ )}
+
+
+
+ {!!startDate && (
+
+
+
+
+ {formatDate(startDate, "dddd, MMMM Do")}
+
+ {!!endDate ? (
+
{`${formatDate(
+ startDate,
+ "h:mm a",
+ )} to ${formatDate(endDate, "h:mm a")}`}
+ ) : (
+
{`${formatDate(
+ startDate,
+ "h:mm a",
+ )}`}
+ )}
+
+
+ )}
+ {!!location && (
+
+
+
+ {location.length > 2 ? (
+ <>
+
{location[1]}
+
+ {location[2]}
+
+ >
+ ) : (
+
{location[0]}
+ )}
+
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/app/(app)/event/[naddr]/_components/ProfileInfo.tsx b/app/(app)/event/[naddr]/_components/ProfileInfo.tsx
new file mode 100644
index 0000000..8595e7d
--- /dev/null
+++ b/app/(app)/event/[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)/event/[naddr]/page.tsx b/app/(app)/event/[naddr]/page.tsx
new file mode 100644
index 0000000..92b74cc
--- /dev/null
+++ b/app/(app)/event/[naddr]/page.tsx
@@ -0,0 +1,61 @@
+"use client";
+import { useState } from "react";
+import Image from "next/image";
+import { nip19 } from "nostr-tools";
+import useEvents from "@/lib/hooks/useEvents";
+import Spinner from "@/components/spinner";
+import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
+import Feed from "@/containers/Feed";
+import Header from "./_components/Header";
+
+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 noteIds = getTagsValues("e", event.tags).filter(Boolean);
+
+ return (
+
+ );
+}
diff --git a/components/EventIcons/DateIcon.tsx b/components/EventIcons/DateIcon.tsx
index 9c7f32d..0c7d4e2 100644
--- a/components/EventIcons/DateIcon.tsx
+++ b/components/EventIcons/DateIcon.tsx
@@ -5,7 +5,7 @@ type SmallCalendarIconProps = {
};
export default function SmallCalendarIcon({ date }: SmallCalendarIconProps) {
return (
-
+
{formatDate(date, "MMM")}
diff --git a/components/LocationSearch/index.tsx b/components/LocationSearch/index.tsx
index e6d42af..5fa7358 100644
--- a/components/LocationSearch/index.tsx
+++ b/components/LocationSearch/index.tsx
@@ -119,10 +119,12 @@ function CommandSearch({ location, onSelect }: CommandSearchProps) {
>
{location ? (
- {location.name}
-
- {location.address}
-
+
+ {location.name}
+
+ {location.address}
+
+
) : (
"Add a location..."
diff --git a/components/Modals/CreateCalendarEvent.tsx b/components/Modals/CreateCalendarEvent.tsx
index 5fa6182..1e20e9a 100644
--- a/components/Modals/CreateCalendarEvent.tsx
+++ b/components/Modals/CreateCalendarEvent.tsx
@@ -1,29 +1,40 @@
"use client";
import { useState, useRef, useEffect } from "react";
-import Template from "./Template";
-import { Textarea } from "@/components/ui/textarea";
-import useAutosizeTextArea from "@/lib/hooks/useAutoSizeTextArea";
-import { Button } from "@/components/ui/button";
import { HiX } from "react-icons/hi";
+import { toast } from "sonner";
import { cn } from "@/lib/utils";
-import {
- addMinutesToDate,
- convertToTimezoneDate,
- convertToTimezone,
-} from "@/lib/utils/dates";
+import { randomId } from "@/lib/nostr";
+import { unixTimeNowInSeconds } from "@/lib/nostr/dates";
+import { addMinutesToDate, toUnix, convertToTimezone } from "@/lib/utils/dates";
+
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
import { DatePicker } from "@/components/ui/date-picker";
import { TimePicker } from "@/components/ui/time-picker";
-import { TimezoneSelector } from "../ui/timezone";
-import SmallCalendarIcon from "../EventIcons/DateIcon";
-import LocationIcon from "../EventIcons/LocationIcon";
-import LocationSearchInput from "../LocationSearch";
+import { TimezoneSelector } from "@/components/ui/timezone";
+import { Label } from "@/components/ui/label";
+
+import SmallCalendarIcon from "@/components/EventIcons/DateIcon";
+import LocationIcon from "@/components/EventIcons/LocationIcon";
+import LocationSearchInput from "@/components/LocationSearch";
+
+import useAutosizeTextArea from "@/lib/hooks/useAutoSizeTextArea";
import { useModal } from "@/app/_providers/modal/provider";
+import { useRouter } from "next/navigation";
+import { createEvent } from "@/lib/actions/create";
+import { useNDK } from "@/app/_providers/ndk";
+import { type NostrEvent } from "@nostr-dev-kit/ndk";
+import useCurrentUser from "@/lib/hooks/useCurrentUser";
export default function CreateCalendarEventModal() {
const modal = useModal();
const now = new Date(new Date().setHours(12, 0, 0, 0));
+ const [isLoading, setIsLoading] = useState(false);
+
+ const [error, setError] = useState("");
const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
const [startDate, setStartDate] = useState
(now);
const startTime = `${
startDate?.getHours().toLocaleString().length === 1
@@ -34,7 +45,7 @@ export default function CreateCalendarEventModal() {
? "0" + startDate?.getMinutes().toLocaleString()
: startDate?.getMinutes()
}`;
- const [endDate, setEndDate] = useState(
+ const [endDate, setEndDate] = useState(
new Date(new Date().setHours(13)),
);
const endTime = `${
@@ -54,6 +65,65 @@ export default function CreateCalendarEventModal() {
name: string;
coordinates: { lat: number; lng: number };
}>();
+ const { ndk } = useNDK();
+ const { currentUser } = useCurrentUser();
+ const router = useRouter();
+
+ async function handleSubmit() {
+ console.log("CALLED", ndk, currentUser);
+ if (!ndk || !currentUser) return;
+ setIsLoading(true);
+ if (!title) {
+ setError("Please add a title");
+ return;
+ }
+ try {
+ const random = randomId();
+
+ const tags: string[][] = [
+ ["d", random],
+ ["name", title],
+ ["description", description],
+ ["start", toUnix(convertToTimezone(startDate, timezone)).toString()],
+ ["end", toUnix(convertToTimezone(endDate, timezone)).toString()],
+ ["start_tzid", timezone],
+ ];
+ if (location) {
+ tags.push([
+ "location",
+ `${location.name}, ${location.address}`,
+ location.name,
+ location.address,
+ ]);
+ tags.push([
+ "address",
+ `${location.name}, ${location.address}`,
+ location.name,
+ location.address,
+ ]);
+ }
+ console.log("Adding ", tags);
+ const preEvent = {
+ content: description,
+ pubkey: currentUser.pubkey,
+ created_at: unixTimeNowInSeconds(),
+ tags: tags,
+ kind: 31923,
+ };
+ const event = await createEvent(ndk, preEvent);
+ if (event) {
+ toast.success("Event Created!");
+ modal?.hide();
+ router.push(`/event/${event.encode()}`);
+ } else {
+ toast.error("An error occured");
+ }
+ } catch (err) {
+ console.log("err", err);
+ } finally {
+ setIsLoading(false);
+ }
+ }
useEffect(() => {
if (startDate && endDate) {
@@ -99,7 +169,7 @@ export default function CreateCalendarEventModal() {
-
Start
+
Start
-
End
+
End
+
+
+
+
+
+
diff --git a/lib/nostr/utils.ts b/lib/nostr/utils.ts
index 0a56e75..a6e1ce1 100644
--- a/lib/nostr/utils.ts
+++ b/lib/nostr/utils.ts
@@ -51,8 +51,7 @@ export const getTagValues = (name: string, tags: string[][]) => {
export const getTagAllValues = (name: string, tags: string[][]) => {
const [itemTag] = tags.filter((tag: string[]) => tag[0] === name);
const itemValues = itemTag || [, undefined];
- itemValues.shift();
- return itemValues;
+ return itemValues.map((i, idx) => (idx ? i : undefined)).filter(Boolean);
};
export const getTagsValues = (name: string, tags: string[][]) => {
const itemTags = tags.filter((tag: string[]) => tag[0] === name);
diff --git a/lib/utils/dates.ts b/lib/utils/dates.ts
index ba3c6e0..3f3ee37 100644
--- a/lib/utils/dates.ts
+++ b/lib/utils/dates.ts
@@ -80,6 +80,11 @@ export function formatDate(timestamp: Date, format?: string) {
dayjs.extend(timezone);
return dayjs(timestamp).format(format ?? "MMMM Do, YYYY");
}
+export function formatDateUnix(timestamp: number, format?: string) {
+ dayjs.extend(advancedFormat);
+ dayjs.extend(timezone);
+ return dayjs(timestamp * 1000).format(format ?? "MMMM Do, YYYY");
+}
export function convertToTimezoneDate(inputDate: Date, _timezone: string) {
dayjs.extend(utc);
dayjs.extend(timezone);