event page upgrades
This commit is contained in:
parent
d0389ee84c
commit
b84845eade
@ -1,26 +1,59 @@
|
||||
"use client";
|
||||
import { HiSignal } from "react-icons/hi2";
|
||||
import { RiAddFill } from "react-icons/ri";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Feed from "@/containers/Feed";
|
||||
import CreateKind1Modal from "@/components/Modals/Kind1";
|
||||
|
||||
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||
import { useModal } from "@/app/_providers/modal/provider";
|
||||
|
||||
type AnnouncementsContainerProps = {
|
||||
eventReference: string;
|
||||
hosts: string[];
|
||||
};
|
||||
export default function AnnouncementsContainer({
|
||||
eventReference,
|
||||
hosts,
|
||||
}: AnnouncementsContainerProps) {
|
||||
const modal = useModal();
|
||||
|
||||
const { currentUser } = useCurrentUser();
|
||||
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 className="flex items-center justify-between gap-x-3 pb-2 pl-2">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<HiSignal className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Announcements</h3>
|
||||
</div>
|
||||
{currentUser && hosts.includes(currentUser.pubkey) && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
modal?.show(
|
||||
<CreateKind1Modal
|
||||
tags={[
|
||||
["a", eventReference],
|
||||
["l", "announcement"],
|
||||
]}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
size={"icon"}
|
||||
className="center group h-auto w-auto gap-x-1 rounded-full p-1"
|
||||
>
|
||||
<RiAddFill className="h-5 w-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full space-y-3">
|
||||
<Feed
|
||||
filter={{
|
||||
kinds: [1],
|
||||
authors: hosts,
|
||||
["#a"]: [eventReference],
|
||||
["#l"]: ["announcement"],
|
||||
}}
|
||||
empty={() => (
|
||||
<div className="py-5 text-center text-muted-foreground">
|
||||
<div className="py-3 text-center text-sm text-muted-foreground">
|
||||
<p>No Announcements yet</p>
|
||||
</div>
|
||||
)}
|
||||
|
48
app/(app)/event/[naddr]/_components/DiscussionContainer.tsx
Normal file
48
app/(app)/event/[naddr]/_components/DiscussionContainer.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
import { HiOutlineChatBubbleLeftRight } from "react-icons/hi2";
|
||||
import { RiAddFill } from "react-icons/ri";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Feed from "@/containers/Feed";
|
||||
import CreateKind1Modal from "@/components/Modals/Kind1";
|
||||
import { useModal } from "@/app/_providers/modal/provider";
|
||||
|
||||
type DiscussionContainerProps = {
|
||||
eventReference: string;
|
||||
};
|
||||
export default function DiscussionContainer({
|
||||
eventReference,
|
||||
}: DiscussionContainerProps) {
|
||||
const modal = useModal();
|
||||
return (
|
||||
<div className="overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem]">
|
||||
<div className="flex items-center justify-between gap-x-3 pb-2 pl-2">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<HiOutlineChatBubbleLeftRight className="h-5 w-5" />
|
||||
<h3 className="text-lg font-semibold">Discussion</h3>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
modal?.show(<CreateKind1Modal tags={[["a", eventReference]]} />)
|
||||
}
|
||||
size={"icon"}
|
||||
className="center group h-auto w-auto gap-x-1 rounded-full p-1"
|
||||
>
|
||||
<RiAddFill className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full space-y-3">
|
||||
<Feed
|
||||
filter={{
|
||||
kinds: [1],
|
||||
["#a"]: [eventReference],
|
||||
}}
|
||||
empty={() => (
|
||||
<div className="py-3 text-center text-sm text-muted-foreground">
|
||||
<p>No Notes yet</p>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -17,6 +17,7 @@ import LocationPreview from "@/components/LocationPreview";
|
||||
import HostsContainer from "./_components/HostsContainer";
|
||||
import LocationContainer from "./_components/LocationContainer";
|
||||
import AnnouncementsContainer from "./_components/AnnouncementsContainer";
|
||||
import DiscussionContainer from "./_components/DiscussionContainer";
|
||||
import AttendeesContainer from "./_components/AttendeesContainer";
|
||||
|
||||
export default function EventPage({
|
||||
@ -50,7 +51,7 @@ export default function EventPage({
|
||||
}
|
||||
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);
|
||||
@ -77,8 +78,12 @@ export default function EventPage({
|
||||
<HostsContainer hosts={hosts} />
|
||||
<AttendeesContainer attendees={attendees} />
|
||||
</div>
|
||||
<div className="max-w-2xl grow">
|
||||
<AnnouncementsContainer eventReference={eventReference} />
|
||||
<div className="max-w-2xl grow space-y-4">
|
||||
<AnnouncementsContainer
|
||||
eventReference={eventReference}
|
||||
hosts={hosts}
|
||||
/>
|
||||
<DiscussionContainer eventReference={eventReference} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -122,6 +122,9 @@
|
||||
.center {
|
||||
@apply flex items-center justify-center;
|
||||
}
|
||||
.invisible-input {
|
||||
@apply resize-none break-words !rounded-none !border-0 !bg-transparent !p-0 text-foreground !shadow-none !outline-none placeholder:text-muted/20 focus-visible:!ring-0;
|
||||
}
|
||||
}
|
||||
input[type="time"]::-webkit-calendar-picker-indicator {
|
||||
background: none;
|
||||
|
@ -75,8 +75,10 @@ export default function Container({
|
||||
<LoadingProfileHeader />
|
||||
)}
|
||||
<div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground">
|
||||
{!!createdAt &&
|
||||
formatDate(new Date(createdAt * 1000), "MMM Do, h:mm a")}
|
||||
<span className="line-clamp-1">
|
||||
{!!createdAt &&
|
||||
formatDate(new Date(createdAt * 1000), "MMM Do, h:mm a")}
|
||||
</span>
|
||||
<DropDownMenu options={actionOptions}>
|
||||
<Button
|
||||
size={"sm"}
|
||||
|
@ -24,7 +24,7 @@ export default function ProfileHeader({ pubkey, locked }: ProfileHeaderProps) {
|
||||
{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">
|
||||
<span className="line-clamp-1 text-sm font-medium text-foreground group-hover:underline">
|
||||
{getNameToShow({ npub, profile })}
|
||||
</span>
|
||||
{!!profile?.nip05 && (
|
||||
@ -34,7 +34,7 @@ export default function ProfileHeader({ pubkey, locked }: ProfileHeaderProps) {
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{!!profile.nip05 && (
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
<span className="line-clamp-1 text-[11px] text-muted-foreground">
|
||||
{profile.nip05}
|
||||
</span>
|
||||
)}
|
||||
@ -42,7 +42,7 @@ export default function ProfileHeader({ pubkey, locked }: ProfileHeaderProps) {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm uppercase text-foreground group-hover:underline">
|
||||
<span className="line-clamp-1 text-sm uppercase text-foreground group-hover:underline">
|
||||
{getNameToShow({ npub, profile })}
|
||||
</span>
|
||||
{!!profile?.nip05 && (
|
||||
|
32
components/Modals/BareTemplate.tsx
Normal file
32
components/Modals/BareTemplate.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HiX } from "react-icons/hi";
|
||||
import { useModal } from "@/app/_providers/modal/provider";
|
||||
|
||||
type ModalProps = React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export default function BareTemplate({ children, className }: ModalProps) {
|
||||
const modal = useModal();
|
||||
|
||||
function handleClose() {
|
||||
modal?.hide();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full grow bg-background p-4 shadow md:rounded-lg md:border md:p-6",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="absolute right-4 top-4 hidden text-muted-foreground transition-all hover:text-primary md:flex"
|
||||
>
|
||||
<HiX className="h-4 w-4" />
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -176,7 +176,7 @@ export default function CreateCalendarEventModal() {
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Event Name"
|
||||
className={cn(
|
||||
"resize-none break-words border-0 bg-transparent p-0 !text-3xl font-bold text-foreground shadow-none outline-none placeholder:text-muted-foreground/50 placeholder:hover:text-muted-foreground/80 focus-visible:ring-0",
|
||||
"invisible-input !text-3xl font-bold text-foreground outline-none placeholder:text-muted-foreground/50 placeholder:hover:text-muted-foreground/80",
|
||||
title === "" && "max-h-[60px]",
|
||||
)}
|
||||
/>
|
||||
|
@ -131,7 +131,7 @@ export default function FormModal<TSchema extends FieldValues>({
|
||||
});
|
||||
const { watch, setValue } = form;
|
||||
return (
|
||||
<Template title={title} className="md:max-w-[400px]">
|
||||
<Template title={title} className="md:max-w-[450px]">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{fields.map(
|
||||
|
129
components/Modals/Kind1.tsx
Normal file
129
components/Modals/Kind1.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { HiX, HiOutlinePaperClip } from "react-icons/hi";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
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 useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||
import useImageUpload from "@/lib/hooks/useImageUpload";
|
||||
|
||||
type CreateKind1ModalProps = {
|
||||
tags?: string[][];
|
||||
};
|
||||
|
||||
export default function CreateKind1Modal({
|
||||
tags: initialTags = [],
|
||||
}: CreateKind1ModalProps) {
|
||||
const modal = useModal();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [content, setContent] = useState("");
|
||||
const {
|
||||
ImageUploadButton,
|
||||
ImagePreview,
|
||||
clear,
|
||||
imagePreview,
|
||||
imageUrl,
|
||||
status: imageStatus,
|
||||
} = useImageUpload("event");
|
||||
const { ndk } = useNDK();
|
||||
const { currentUser } = useCurrentUser();
|
||||
const contentRef = useRef<HTMLTextAreaElement>(null);
|
||||
useAutosizeTextArea(contentRef.current, content);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!ndk || !currentUser) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const tags: string[][] = initialTags;
|
||||
let noteContent = content;
|
||||
|
||||
if (imageUrl) {
|
||||
tags.push(["r", imageUrl]);
|
||||
noteContent += `\n${imageUrl}`;
|
||||
}
|
||||
const preEvent = {
|
||||
content: noteContent,
|
||||
pubkey: currentUser.pubkey,
|
||||
tags: tags,
|
||||
kind: 1,
|
||||
};
|
||||
const event = await createEvent(ndk, preEvent);
|
||||
if (event) {
|
||||
toast.success("Event Created!");
|
||||
modal?.hide();
|
||||
} else {
|
||||
toast.error("An error occured");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full grow bg-background p-4 shadow md:rounded-lg md:border md:p-6",
|
||||
"md:max-w-[450px]",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
onClick={() => modal?.hide()}
|
||||
className="absolute right-4 top-4 hidden text-muted-foreground transition-all hover:text-primary md:flex"
|
||||
>
|
||||
<HiX className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="space-y-4">
|
||||
<div className="">
|
||||
<Textarea
|
||||
ref={contentRef}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="What's on your mind?"
|
||||
autoFocus
|
||||
className={cn(
|
||||
"invisible-input min-h-[60px] text-[1.5rem] font-medium text-foreground placeholder:text-muted-foreground/70",
|
||||
content.length > 50 && "text-[1.25rem]",
|
||||
content.length > 120 && "text-[1rem]",
|
||||
)}
|
||||
/>
|
||||
<div className="mt-1 flex items-center text-muted-foreground">
|
||||
{imagePreview ? (
|
||||
<ImagePreview className="" />
|
||||
) : (
|
||||
<ImageUploadButton>
|
||||
<Button
|
||||
size={"icon"}
|
||||
variant={"outline"}
|
||||
className="rounded-full"
|
||||
>
|
||||
<HiOutlinePaperClip className="h-4 w-4" />
|
||||
</Button>
|
||||
</ImageUploadButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
disabled={imageStatus === "uploading"}
|
||||
className="w-full"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
"use client";
|
||||
import { useState, ReactNode, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import { z } from "zod";
|
||||
import { createZodFetcher } from "zod-fetch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Spinner from "@/components/spinner";
|
||||
import { HiX } from "react-icons/hi";
|
||||
|
||||
const fetchWithZod = createZodFetcher();
|
||||
|
||||
@ -92,6 +96,40 @@ const useImageUpload = (folderName?: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ImagePreview = ({ className }: { className?: string }) => {
|
||||
if (!imagePreview) return null;
|
||||
return (
|
||||
<div className={cn("relative overflow-hidden rounded-xl", className)}>
|
||||
<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]",
|
||||
status === "uploading" && "grayscale",
|
||||
status === "error" && "blur-xl",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{status === "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>
|
||||
)}
|
||||
{status === "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>
|
||||
);
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
setStatus("empty");
|
||||
setImageUrl(null);
|
||||
@ -103,6 +141,7 @@ const useImageUpload = (folderName?: string) => {
|
||||
status,
|
||||
imageUrl,
|
||||
ImageUploadButton,
|
||||
ImagePreview,
|
||||
clear,
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user