improving events page

This commit is contained in:
zmeyer44 2023-10-25 12:56:51 -04:00
parent 181be8c902
commit d0389ee84c
11 changed files with 357 additions and 55 deletions

View File

@ -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>
);
}

View 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>
);
}

View File

@ -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>
)}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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>

View 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...")}
/>
);
}

View File

@ -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) {

View 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>
);
}