Adding calendars
This commit is contained in:
parent
b1ed914eb7
commit
963ee4cfc0
19
app/(app)/calendar/[naddr]/_components/CreateEventButton.tsx
Normal file
19
app/(app)/calendar/[naddr]/_components/CreateEventButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
158
app/(app)/calendar/[naddr]/_components/Header.tsx
Normal file
158
app/(app)/calendar/[naddr]/_components/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
44
app/(app)/calendar/[naddr]/_components/ProfileInfo.tsx
Normal file
44
app/(app)/calendar/[naddr]/_components/ProfileInfo.tsx
Normal 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>
|
||||
);
|
||||
}
|
70
app/(app)/calendar/[naddr]/page.tsx
Normal file
70
app/(app)/calendar/[naddr]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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({
|
||||
</div>
|
||||
<div className="w-full space-y-3">
|
||||
<Feed
|
||||
secondaryFilter={(e) => getTagValues("l", e.tags) !== "announcement"}
|
||||
filter={{
|
||||
kinds: [1],
|
||||
["#a"]: [eventReference],
|
||||
|
@ -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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
import { type NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import EventsTimeline from "@/containers/EventsTimeline";
|
||||
export default function EventsPage() {
|
||||
return (
|
||||
<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">
|
||||
@ -33,40 +9,8 @@ export default function Page() {
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mx-auto max-w-[900px] space-y-6">
|
||||
{eventsByDay.map((e) => (
|
||||
<CalendarSection events={e} />
|
||||
))}
|
||||
<EventsTimeline filter={{ kinds: [31923 as NDKKind], limit: 100 }} />
|
||||
</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;
|
||||
}
|
||||
|
146
app/(app)/explore/_components/CalendarCard.tsx
Normal file
146
app/(app)/explore/_components/CalendarCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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.
|
||||
</div>
|
||||
<Button onClick={() => modal?.show(<CreateSubscriptionTier />)}>
|
||||
Become a Creator
|
||||
<Button onClick={() => modal?.show(<CreateCalendarEvent />)}>
|
||||
Create an Event
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
72
app/(app)/explore/_sections/ExploreCalendars.tsx
Normal file
72
app/(app)/explore/_sections/ExploreCalendars.tsx
Normal 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} />;
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import ExploreCreators from "./_sections/ExploreCreators";
|
||||
import ExploreCalendars from "./_sections/ExploreCalendars";
|
||||
import UpcomingEvents from "./_sections/UpcomingEvents";
|
||||
import LongFormContentSection from "./_sections/LongFormContent";
|
||||
import CreateEvents from "./_sections/CreateEvents";
|
||||
@ -23,9 +24,9 @@ const NewEventButton = dynamic(() => import("./_components/NewEventButton"), {
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="relative space-y-6 px-5 pt-5 sm:pt-7">
|
||||
<ExploreCreators />
|
||||
<ExploreCalendars />
|
||||
<UpcomingEvents />
|
||||
<LongFormContentSection />
|
||||
{/* <LongFormContentSection /> */}
|
||||
<CreateEvents />
|
||||
<LiveStreamingSection />
|
||||
<FeaturedListsSection />
|
||||
|
@ -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="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">
|
||||
Own your following. Only on Nostr.
|
||||
Own your Events. Only on Nostr.
|
||||
</h1>
|
||||
<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
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { formatDate, fromUnix } from "@/lib/utils/dates";
|
||||
import { BANNER } from "@/constants";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
@ -13,7 +13,11 @@ import {
|
||||
import SmallProfileLine from "@/components/ProfileContainers/SmallProfileLine";
|
||||
import AvatarStack from "@/components/ProfileContainers/AvatarStack";
|
||||
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 { RxClock, RxCalendar } from "react-icons/rx";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@ -28,7 +32,8 @@ export default function LargeFeedCard({ event }: LargeFeedCardProps) {
|
||||
const image = getTagValues("image", tags);
|
||||
const location =
|
||||
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)
|
||||
? new Date(parseInt(getTagValues("start", tags) as string) * 1000)
|
||||
: null;
|
||||
@ -58,7 +63,7 @@ export default function LargeFeedCard({ event }: LargeFeedCardProps) {
|
||||
<HiOutlineUserCircle className="h-4 w-4 text-primary" />
|
||||
<AvatarStack
|
||||
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}
|
||||
/>
|
||||
</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-3/4 items-center justify-stretch gap-3">
|
||||
<Button className="flex-1">RSVP</Button>
|
||||
<Link href={`/event/${event.encode()}`}>
|
||||
<Button variant={"outline"} className="flex-1">
|
||||
Share
|
||||
Details
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
88
components/FormComponents/Picker.tsx
Normal file
88
components/FormComponents/Picker.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { HiX } from "react-icons/hi";
|
||||
import { HiOutlineCalendarDays } from "react-icons/hi2";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { randomId } from "@/lib/nostr";
|
||||
@ -15,6 +16,7 @@ import { DatePicker } from "@/components/ui/date-picker";
|
||||
import { TimePicker } from "@/components/ui/time-picker";
|
||||
import { TimezoneSelector } from "@/components/ui/timezone";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Picker from "@/components/FormComponents/Picker";
|
||||
|
||||
import SmallCalendarIcon from "@/components/EventIcons/DateIcon";
|
||||
import LocationIcon from "@/components/EventIcons/LocationIcon";
|
||||
@ -24,10 +26,12 @@ import Spinner from "../spinner";
|
||||
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 { createEvent, updateList } from "@/lib/actions/create";
|
||||
import { useNDK } from "@/app/_providers/ndk";
|
||||
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||
import useImageUpload from "@/lib/hooks/useImageUpload";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
import { getTagValues } from "@/lib/nostr/utils";
|
||||
|
||||
export default function CreateCalendarEventModal() {
|
||||
const modal = useModal();
|
||||
@ -66,6 +70,7 @@ export default function CreateCalendarEventModal() {
|
||||
const [timezone, setTimezone] = useState(
|
||||
Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
);
|
||||
const [calendar, setCalendar] = useState<string>();
|
||||
const [location, setLocation] = useState<{
|
||||
address: string;
|
||||
name: string;
|
||||
@ -73,11 +78,10 @@ export default function CreateCalendarEventModal() {
|
||||
geohash: string;
|
||||
}>();
|
||||
const { ndk } = useNDK();
|
||||
const { currentUser } = useCurrentUser();
|
||||
const { currentUser, calendars } = useCurrentUser();
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSubmit() {
|
||||
console.log("CALLED", ndk, currentUser);
|
||||
if (!ndk || !currentUser) {
|
||||
alert("MISSING");
|
||||
return;
|
||||
@ -130,9 +134,21 @@ export default function CreateCalendarEventModal() {
|
||||
};
|
||||
const event = await createEvent(ndk, preEvent);
|
||||
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!");
|
||||
modal?.hide();
|
||||
router.push(`/event/${event.encode()}`);
|
||||
router.push(`/event/${encodedEvent}`);
|
||||
} else {
|
||||
toast.error("An error occured");
|
||||
}
|
||||
@ -279,6 +295,79 @@ export default function CreateCalendarEventModal() {
|
||||
</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 className="flex w-full items-start gap-x-3">
|
||||
@ -307,7 +396,7 @@ export default function CreateCalendarEventModal() {
|
||||
placeholder="Some into about this event..."
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-end sm:hidden">
|
||||
{imagePreview ? (
|
||||
<div className="relative overflow-hidden rounded-xl">
|
||||
<div className="">
|
||||
|
@ -19,9 +19,13 @@ export default function AvatarStack({
|
||||
}: AvatarStackProps) {
|
||||
return (
|
||||
<div className="isolate flex -space-x-2 overflow-hidden py-[2px]">
|
||||
{pubkeys.map((p, idx) => (
|
||||
{pubkeys.map((p, idx) => {
|
||||
if (p) {
|
||||
return (
|
||||
<User key={p} pubkey={p} className={cn(zIndexes[idx], className)} />
|
||||
))}
|
||||
);
|
||||
}
|
||||
})}
|
||||
{!!remaining && (
|
||||
<Avatar
|
||||
className={cn(
|
||||
|
@ -7,6 +7,15 @@ export const EXPLORE_CREATORS = [
|
||||
"npub1dc9p7jzjhj86g2uqgltq4qvnpkyfqn9r72kdlddcgyat3j05gnjsgjc8rz",
|
||||
"npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx",
|
||||
];
|
||||
export const EXPLORE_CALENDARS = [
|
||||
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
||||
"npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
|
||||
"npub1u6qhg5ucu3xza4nlz94q90y720tr6l09avnq8y3yfp5qrv9v8sus3tnd7t",
|
||||
"npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m",
|
||||
"npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk",
|
||||
"npub1dc9p7jzjhj86g2uqgltq4qvnpkyfqn9r72kdlddcgyat3j05gnjsgjc8rz",
|
||||
"npub1qny3tkh0acurzla8x3zy4nhrjz5zd8l9sy9jys09umwng00manysew95gx",
|
||||
];
|
||||
export const NOTABLE_ACCOUNTS = [
|
||||
"npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s",
|
||||
"npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft",
|
||||
|
132
containers/EventsTimeline/CalendarSection.tsx
Normal file
132
containers/EventsTimeline/CalendarSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
80
containers/EventsTimeline/EventsFromCalendar.tsx
Normal file
80
containers/EventsTimeline/EventsFromCalendar.tsx
Normal 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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
73
containers/EventsTimeline/index.tsx
Normal file
73
containers/EventsTimeline/index.tsx
Normal 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;
|
||||
}
|
@ -4,9 +4,10 @@ import { cn } from "@/lib/utils";
|
||||
import Spinner from "@/components/spinner";
|
||||
import { Event } from "nostr-tools";
|
||||
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 = {
|
||||
filter?: NDKFilter;
|
||||
secondaryFilter?: (event: NDKEvent) => Boolean;
|
||||
className?: string;
|
||||
loader?: () => JSX.Element;
|
||||
empty?: () => JSX.Element;
|
||||
@ -14,7 +15,7 @@ type FeedProps = {
|
||||
|
||||
export default function Feed({
|
||||
filter,
|
||||
className,
|
||||
secondaryFilter,
|
||||
loader: Loader,
|
||||
empty: Empty,
|
||||
}: FeedProps) {
|
||||
@ -30,6 +31,16 @@ export default function Feed({
|
||||
if (Empty && events.length === 0) {
|
||||
return <Empty />;
|
||||
}
|
||||
if (secondaryFilter) {
|
||||
return (
|
||||
<>
|
||||
{events.filter(secondaryFilter).map((e) => {
|
||||
const event = e.rawEvent() as Event;
|
||||
return <KindCard key={e.id} {...event} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{events.map((e) => {
|
||||
|
@ -7,7 +7,7 @@ import { useNDK } from "@/app/_providers/ndk";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import useLists from "./useLists";
|
||||
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";
|
||||
const loadNWCUrl = "";
|
||||
const nwc = new webln.NWC({ nostrWalletConnectUrl: loadNWCUrl });
|
||||
@ -20,6 +20,8 @@ export default function useCurrentUser() {
|
||||
follows,
|
||||
setFollows,
|
||||
addFollow,
|
||||
calendars,
|
||||
setCalendars,
|
||||
} = currentUserStore();
|
||||
const { loginWithNip07, loginWithNip46, getProfile, ndk, fetchEvents } =
|
||||
useNDK();
|
||||
@ -86,14 +88,24 @@ export default function useCurrentUser() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser) return;
|
||||
console.log("fetching follows");
|
||||
console.log("fetching follows & calendar");
|
||||
(async () => {
|
||||
const following = await currentUser.follows();
|
||||
console.log("Follows", following);
|
||||
setFollows(following);
|
||||
await fetchCalendars();
|
||||
})();
|
||||
}, [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 {
|
||||
currentUser,
|
||||
isLoading: false,
|
||||
@ -107,5 +119,7 @@ export default function useCurrentUser() {
|
||||
mySubscription,
|
||||
addFollow,
|
||||
setFollows,
|
||||
calendars,
|
||||
fetchCalendars,
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { create } from "zustand";
|
||||
import { type NDKUser } from "@nostr-dev-kit/ndk";
|
||||
import { NDKEvent, type NDKUser } from "@nostr-dev-kit/ndk";
|
||||
|
||||
type Settings = {};
|
||||
|
||||
interface CurrentUserState {
|
||||
currentUser: NDKUser | null;
|
||||
follows: Set<NDKUser>;
|
||||
calendars: Set<NDKEvent>;
|
||||
setCalendars: (calendars: Set<NDKEvent>) => void;
|
||||
settings: Settings;
|
||||
setCurrentUser: (user: NDKUser | null) => void;
|
||||
updateCurrentUser: (user: Partial<NDKUser>) => void;
|
||||
@ -16,6 +18,7 @@ interface CurrentUserState {
|
||||
const currentUserStore = create<CurrentUserState>()((set) => ({
|
||||
currentUser: null,
|
||||
follows: new Set(),
|
||||
calendars: new Set(),
|
||||
settings: {},
|
||||
setCurrentUser: (user) => set((state) => ({ ...state, currentUser: user })),
|
||||
updateCurrentUser: (user) =>
|
||||
@ -24,6 +27,8 @@ const currentUserStore = create<CurrentUserState>()((set) => ({
|
||||
currentUser: { ...state.currentUser, ...user } as NDKUser,
|
||||
})),
|
||||
setFollows: (follows) => set((state) => ({ ...state, follows: follows })),
|
||||
setCalendars: (calendars) =>
|
||||
set((state) => ({ ...state, calendars: calendars })),
|
||||
addFollow: (follow) =>
|
||||
set((state) => ({ ...state, follows: new Set(state.follows).add(follow) })),
|
||||
}));
|
||||
|
Loading…
x
Reference in New Issue
Block a user