improving events page
This commit is contained in:
parent
181be8c902
commit
d0389ee84c
@ -0,0 +1,31 @@
|
||||
import { HiSignal } from "react-icons/hi2";
|
||||
import Feed from "@/containers/Feed";
|
||||
|
||||
type AnnouncementsContainerProps = {
|
||||
eventReference: string;
|
||||
};
|
||||
export default function AnnouncementsContainer({
|
||||
eventReference,
|
||||
}: AnnouncementsContainerProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem]">
|
||||
<div className="flex items-center gap-x-3 px-2 pb-2">
|
||||
<HiSignal className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Announcements</h3>
|
||||
</div>
|
||||
<div className="w-full space-y-3">
|
||||
<Feed
|
||||
filter={{
|
||||
kinds: [1],
|
||||
["#a"]: [eventReference],
|
||||
}}
|
||||
empty={() => (
|
||||
<div className="py-5 text-center text-muted-foreground">
|
||||
<p>No Announcements yet</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
23
app/(app)/event/[naddr]/_components/AttendeesContainer.tsx
Normal file
23
app/(app)/event/[naddr]/_components/AttendeesContainer.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { HiOutlineUserGroup } from "react-icons/hi2";
|
||||
|
||||
import UserRow from "./UserRow";
|
||||
type AttendeesContainerProps = {
|
||||
attendees: string[];
|
||||
};
|
||||
export default function AttendeesContainer({
|
||||
attendees,
|
||||
}: AttendeesContainerProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem]">
|
||||
<div className="flex items-center gap-x-3 px-2 pb-2">
|
||||
<HiOutlineUserGroup className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Attendees</h3>
|
||||
</div>
|
||||
<ul className="max-h-[200px] overflow-hidden">
|
||||
{attendees.map((pubkey) => (
|
||||
<UserRow key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -27,7 +27,7 @@ import { formatDate } from "@/lib/utils/dates";
|
||||
import SmallCalendarIcon from "@/components/EventIcons/DateIcon";
|
||||
import LocationIcon from "@/components/EventIcons/LocationIcon";
|
||||
|
||||
const EditEventModal = dynamic(() => import("@/components/Modals/EditEvent"), {
|
||||
const RSVPButton = dynamic(() => import("./RSVPButton"), {
|
||||
ssr: false,
|
||||
});
|
||||
const CreateListEvent = dynamic(
|
||||
@ -48,7 +48,7 @@ export default function Header({ event }: { event: NDKEvent }) {
|
||||
const [hasValidPayment, setHasValidPayment] = useState(false);
|
||||
const { pubkey, tags } = event;
|
||||
const { profile } = useProfile(pubkey);
|
||||
|
||||
const eventReference = event.encode();
|
||||
const title = getTagValues("name", tags) ?? "Untitled";
|
||||
const image =
|
||||
getTagValues("image", tags) ??
|
||||
@ -170,35 +170,7 @@ export default function Header({ event }: { event: NDKEvent }) {
|
||||
</Button>
|
||||
</>
|
||||
)} */}
|
||||
<Button
|
||||
// onClick={() =>
|
||||
// modal?.show(
|
||||
// <ConfirmModal
|
||||
// title={`Subscribe to ${title}`}
|
||||
// onConfirm={handleSendZap}
|
||||
// ctaBody={
|
||||
// <>
|
||||
// <span>Zap to Subscribe</span>
|
||||
// <HiOutlineLightningBolt className="h-4 w-4" />
|
||||
// </>
|
||||
// }
|
||||
// >
|
||||
// <p className="text-muted-forground">
|
||||
// {`Pay ${priceInBTC} BTC (${formatNumber(
|
||||
// btcToSats(priceInBTC),
|
||||
// )} sats) for year long access until ${formatDate(
|
||||
// new Date(
|
||||
// new Date().setFullYear(new Date().getFullYear() + 1),
|
||||
// ),
|
||||
// "MMM Do, YYYY",
|
||||
// )}`}
|
||||
// </p>
|
||||
// </ConfirmModal>,
|
||||
// )
|
||||
// }
|
||||
>
|
||||
RSVP
|
||||
</Button>
|
||||
<RSVPButton eventReference={eventReference} />
|
||||
{/* {!isMember &&
|
||||
(hasValidPayment ? (
|
||||
<Button variant={"outline"}>Pending Sync</Button>
|
||||
@ -240,7 +212,7 @@ export default function Header({ event }: { event: NDKEvent }) {
|
||||
<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="line-clamp-3 text-sm text-muted-foreground @md:text-sm">
|
||||
<p className="text-sm text-muted-foreground @md:text-sm">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
21
app/(app)/event/[naddr]/_components/HostsContainer.tsx
Normal file
21
app/(app)/event/[naddr]/_components/HostsContainer.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { HiOutlineUsers } from "react-icons/hi2";
|
||||
|
||||
import UserRow from "./UserRow";
|
||||
type HostsContainerProps = {
|
||||
hosts: string[];
|
||||
};
|
||||
export default function HostsContainer({ hosts }: HostsContainerProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem]">
|
||||
<div className="flex items-center gap-x-3 px-2 pb-2">
|
||||
<HiOutlineUsers className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Hosts</h3>
|
||||
</div>
|
||||
<ul>
|
||||
{hosts.map((pubkey) => (
|
||||
<UserRow key={pubkey} pubkey={pubkey} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
26
app/(app)/event/[naddr]/_components/LocationContainer.tsx
Normal file
26
app/(app)/event/[naddr]/_components/LocationContainer.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import LocationBoxRaw from "@/components/LocationPreview/LocationBoxRaw";
|
||||
import { HiOutlineMapPin, HiCheckBadge, HiOutlineUsers } from "react-icons/hi2";
|
||||
|
||||
type LocationContainerProps = {
|
||||
address: string;
|
||||
geohash: string;
|
||||
};
|
||||
export default function LocationContainer({
|
||||
address,
|
||||
geohash,
|
||||
}: LocationContainerProps) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
|
||||
<div className="flex items-center gap-x-3 px-2 pb-2">
|
||||
<HiOutlineMapPin className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Location</h3>
|
||||
</div>
|
||||
<div className="h-[150px] overflow-hidden rounded-lg">
|
||||
<LocationBoxRaw geohash={geohash} address={address} />
|
||||
</div>
|
||||
<div className="flex items-center @lg:px-2 @lg:pt-1">
|
||||
<p className="pt-1.5 text-xs text-muted-foreground">{address}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
19
app/(app)/event/[naddr]/_components/RSVPButton.tsx
Normal file
19
app/(app)/event/[naddr]/_components/RSVPButton.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
onClick={() => modal?.show(<RSVPModal eventReference={eventReference} />)}
|
||||
>
|
||||
RSVP
|
||||
</Button>
|
||||
);
|
||||
}
|
51
app/(app)/event/[naddr]/_components/UserRow.tsx
Normal file
51
app/(app)/event/[naddr]/_components/UserRow.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import Link from "next/link";
|
||||
import { HiOutlineMapPin, HiCheckBadge, HiOutlineUsers } from "react-icons/hi2";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import useProfile from "@/lib/hooks/useProfile";
|
||||
import { getTwoLetters, getNameToShow } from "@/lib/utils";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
export default function UserRow({ pubkey }: { pubkey: string }) {
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const { profile } = useProfile(pubkey);
|
||||
return (
|
||||
<li className="flex items-center">
|
||||
<Link href={`/${pubkey}`} className="center group gap-x-3">
|
||||
<Avatar className="center h-9 w-9 overflow-hidden rounded-sm bg-muted @md:h-10 @md:w-10">
|
||||
<AvatarImage src={profile?.image} alt={profile?.displayName} />
|
||||
<AvatarFallback className="text-xs">
|
||||
{getTwoLetters({ npub, profile })}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{profile?.displayName || profile?.name ? (
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium text-foreground group-hover:underline">
|
||||
{getNameToShow({ npub, profile })}
|
||||
</span>
|
||||
{!!profile?.nip05 && (
|
||||
<HiCheckBadge className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{!!profile.nip05 && (
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{profile.nip05}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm uppercase text-foreground group-hover:underline">
|
||||
{getNameToShow({ npub, profile })}
|
||||
</span>
|
||||
{!!profile?.nip05 && (
|
||||
<HiCheckBadge className="h-4 w-4 text-primary" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
@ -1,17 +1,23 @@
|
||||
"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 Feed from "@/containers/Feed";
|
||||
|
||||
import Header from "./_components/Header";
|
||||
import LocationPreview from "@/components/LocationPreview";
|
||||
import HostsContainer from "./_components/HostsContainer";
|
||||
import LocationContainer from "./_components/LocationContainer";
|
||||
import AnnouncementsContainer from "./_components/AnnouncementsContainer";
|
||||
import AttendeesContainer from "./_components/AttendeesContainer";
|
||||
|
||||
export default function EventPage({
|
||||
params: { naddr },
|
||||
@ -42,35 +48,37 @@ export default function EventPage({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const noteIds = getTagsValues("e", event.tags).filter(Boolean);
|
||||
const location = getTagAllValues("location", event.tags)[0]
|
||||
? getTagAllValues("location", event.tags)
|
||||
: getTagAllValues("address", event.tags);
|
||||
const geohash = getTagValues("g", event.tags);
|
||||
const { tags } = event;
|
||||
const eventReference = event.encode();
|
||||
const noteIds = getTagsValues("e", tags).filter(Boolean);
|
||||
const location = getTagAllValues("location", tags)[0]
|
||||
? getTagAllValues("location", tags)
|
||||
: getTagAllValues("address", tags);
|
||||
const geohash = getTagValues("g", tags);
|
||||
const hosts = getTagsAllValues("p", tags)
|
||||
.filter(([pubkey, relay, role]) => role === "host")
|
||||
.map(([pubkey]) => pubkey)
|
||||
.filter(Boolean);
|
||||
const attendees = getTagsAllValues("p", tags)
|
||||
.map(([pubkey]) => pubkey)
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<div className="relative mx-auto max-w-5xl space-y-4 p-2 sm:p-4">
|
||||
<div className="relative mx-auto max-w-5xl space-y-4 p-2 @container sm:p-4">
|
||||
<Header event={event} />
|
||||
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
|
||||
<div className="flex flex-col gap-3 @xl:flex-row-reverse">
|
||||
<div className="flex flex-col gap-4 @2xl:flex-row-reverse">
|
||||
<div className="flex min-w-[250px] flex-1 flex-col gap-4">
|
||||
{!!location && !!geohash && (
|
||||
<LocationPreview
|
||||
geohash={geohash}
|
||||
<LocationContainer
|
||||
address={location[0] as string}
|
||||
geohash={geohash}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 space-y-3 overflow-hidden rounded-[0.5rem] p-0">
|
||||
<Feed
|
||||
filter={{
|
||||
ids: noteIds,
|
||||
}}
|
||||
empty={() => (
|
||||
<div className="py-5 text-center text-muted-foreground">
|
||||
<p>No Announcements yet</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<HostsContainer hosts={hosts} />
|
||||
<AttendeesContainer attendees={attendees} />
|
||||
</div>
|
||||
<div className="max-w-2xl grow">
|
||||
<AnnouncementsContainer eventReference={eventReference} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
58
components/LocationPreview/LocationBoxRaw.tsx
Normal file
58
components/LocationPreview/LocationBoxRaw.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useLoadScript, GoogleMap } from "@react-google-maps/api";
|
||||
import type { NextPage } from "next";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Geohash from "latlon-geohash";
|
||||
import { HiMapPin } from "react-icons/hi2";
|
||||
|
||||
type LocationPreviewProps = {
|
||||
geohash: string;
|
||||
address: string;
|
||||
className?: string;
|
||||
};
|
||||
export default function LocationBoxRaw({
|
||||
geohash,
|
||||
address,
|
||||
className,
|
||||
}: LocationPreviewProps) {
|
||||
const libraries = useMemo(() => ["places"], []);
|
||||
const { lat, lon } = Geohash.decode(geohash);
|
||||
const mapCenter = useMemo(() => ({ lat, lng: lon }), []);
|
||||
|
||||
const mapOptions = useMemo<google.maps.MapOptions>(
|
||||
() => ({
|
||||
disableDefaultUI: true,
|
||||
clickableIcons: true,
|
||||
scrollwheel: false,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const { isLoaded } = useLoadScript({
|
||||
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY as string,
|
||||
libraries: libraries as any,
|
||||
});
|
||||
|
||||
if (!isLoaded) {
|
||||
return <div className="h-full w-full bg-muted"></div>;
|
||||
}
|
||||
return (
|
||||
<GoogleMap
|
||||
options={mapOptions}
|
||||
zoom={14}
|
||||
center={mapCenter}
|
||||
mapTypeId={google.maps.MapTypeId.ROADMAP}
|
||||
mapContainerStyle={{ width: "100%", height: "100%" }}
|
||||
onLoad={() => console.log("Map Component Loaded...")}
|
||||
/>
|
||||
);
|
||||
}
|
@ -98,6 +98,7 @@ export default function CreateCalendarEventModal() {
|
||||
["start", toUnix(convertToTimezone(startDate, timezone)).toString()],
|
||||
["end", toUnix(convertToTimezone(endDate, timezone)).toString()],
|
||||
["start_tzid", timezone],
|
||||
["p", currentUser.pubkey, "", "host"],
|
||||
];
|
||||
|
||||
if (location) {
|
||||
|
92
components/Modals/RSVP.tsx
Normal file
92
components/Modals/RSVP.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import Template from "./Template";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { useModal } from "@/app/_providers/modal/provider";
|
||||
import { randomId } from "@/lib/nostr";
|
||||
import { unixTimeNowInSeconds } from "@/lib/nostr/dates";
|
||||
// import { useKeys } from "@/app/_providers/keysProvider";
|
||||
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||
import { createEvent } from "@/lib/actions/create";
|
||||
import { useNDK } from "@/app/_providers/ndk";
|
||||
|
||||
type RSVPModalProps = {
|
||||
eventReference: string;
|
||||
};
|
||||
|
||||
const statusMap = {
|
||||
accept: "accepted",
|
||||
maybe: "tentative",
|
||||
decline: "declined",
|
||||
};
|
||||
|
||||
export default function RSVPModal({ eventReference }: RSVPModalProps) {
|
||||
const modal = useModal();
|
||||
const { ndk } = useNDK();
|
||||
const { currentUser } = useCurrentUser();
|
||||
const [loading, setLoading] = useState({
|
||||
accept: false,
|
||||
maybe: false,
|
||||
decline: false,
|
||||
});
|
||||
|
||||
async function handleRSVP(type: "accept" | "maybe" | "decline") {
|
||||
if (!ndk || !currentUser) return;
|
||||
setLoading((prev) => ({ ...prev, [type]: true }));
|
||||
|
||||
try {
|
||||
const random = randomId();
|
||||
const tags: string[][] = [
|
||||
["d", random],
|
||||
["a", eventReference],
|
||||
["L", "status"],
|
||||
["l", statusMap[type], "status"],
|
||||
];
|
||||
const event = await createEvent(ndk, {
|
||||
content: "",
|
||||
kind: 31925,
|
||||
tags,
|
||||
});
|
||||
if (event) {
|
||||
toast.success("Event Created!");
|
||||
modal?.hide();
|
||||
} else {
|
||||
toast.error("An error occured");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err", err);
|
||||
} finally {
|
||||
setLoading({ accept: false, maybe: false, decline: false });
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Template title="RSVP to Event" className="md:max-w-[400px]">
|
||||
<div className="flex flex-col gap-y-5">
|
||||
<Button
|
||||
loading={loading.accept}
|
||||
onClick={() => handleRSVP("accept")}
|
||||
className="w-full gap-x-1"
|
||||
>
|
||||
<span>Accept</span>
|
||||
</Button>
|
||||
<Button
|
||||
loading={loading.maybe}
|
||||
onClick={() => handleRSVP("maybe")}
|
||||
variant={"outline"}
|
||||
className="w-full gap-x-1"
|
||||
>
|
||||
<span>Maybe</span>
|
||||
</Button>
|
||||
<Button
|
||||
loading={loading.decline}
|
||||
onClick={() => handleRSVP("decline")}
|
||||
variant={"destructive"}
|
||||
className="w-full gap-x-1"
|
||||
>
|
||||
<span>Decline</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user