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 && ( + banner + )} +
+
+
+
+
+

+ {title} +

+
+ +
+
+
+ {!!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 ( +
+
+
+
+ ( +
+

No notes yet

+
+ )} + /> +
+
+
+ ); +} 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
+
+ +