adding actions on list page
This commit is contained in:
parent
7cb2080c74
commit
ab377a78d4
185
app/(app)/list/[naddr]/_components/Header.tsx
Normal file
185
app/(app)/list/[naddr]/_components/Header.tsx
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
"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 { nip19 } from "nostr-tools";
|
||||||
|
import useEvents from "@/lib/hooks/useEvents";
|
||||||
|
import Spinner from "@/components/spinner";
|
||||||
|
import { 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";
|
||||||
|
|
||||||
|
const EditListModal = dynamic(() => import("@/components/Modals/EditList"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
const CreateEventModal = dynamic(() => import("@/components/Modals/NewEvent"), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Header({ naddr }: { naddr: string }) {
|
||||||
|
const { currentUser } = useCurrentUser();
|
||||||
|
const modal = useModal();
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const [sendingZap, setSendingZap] = useState(false);
|
||||||
|
const [checkingPayment, setCheckingPayment] = useState(false);
|
||||||
|
const [hasValidPayment, setHasValidPayment] = useState(false);
|
||||||
|
const [syncingUsers, setSyncingUsers] = useState(false);
|
||||||
|
const { type, data } = nip19.decode(naddr);
|
||||||
|
console.log("PASSED", naddr, data);
|
||||||
|
if (type !== "naddr") {
|
||||||
|
throw new Error("Invalid list");
|
||||||
|
}
|
||||||
|
const { identifier, kind, pubkey } = data;
|
||||||
|
const { profile } = useProfile(pubkey);
|
||||||
|
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 noteIds = getTagsValues("e", event.tags).filter(Boolean);
|
||||||
|
console.log("notes", event.tags);
|
||||||
|
const title =
|
||||||
|
getTagValues("title", event.tags) ??
|
||||||
|
getTagValues("name", event.tags) ??
|
||||||
|
"Untitled";
|
||||||
|
const image =
|
||||||
|
getTagValues("image", event.tags) ??
|
||||||
|
getTagValues("picture", event.tags) ??
|
||||||
|
getTagValues("banner", event.tags) ??
|
||||||
|
profile?.banner;
|
||||||
|
|
||||||
|
const description = getTagValues("description", event.tags);
|
||||||
|
const rawEvent = event.rawEvent();
|
||||||
|
const subscriptionsEnabled = !!getTagValues("subscriptions", rawEvent.tags);
|
||||||
|
const priceInBTC = getTagValues("price", rawEvent.tags);
|
||||||
|
const isMember =
|
||||||
|
currentUser &&
|
||||||
|
getTagsValues("p", rawEvent.tags).includes(currentUser.pubkey);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentUser || !subscriptionsEnabled) return;
|
||||||
|
if (!isMember && !checkingPayment && !hasValidPayment) {
|
||||||
|
void handleCheckPayment();
|
||||||
|
}
|
||||||
|
}, [isMember, currentUser]);
|
||||||
|
|
||||||
|
async function handleCheckPayment() {
|
||||||
|
if (!event) return;
|
||||||
|
setCheckingPayment(true);
|
||||||
|
console.log("Checking payment");
|
||||||
|
try {
|
||||||
|
const result = await checkPayment(
|
||||||
|
ndk!,
|
||||||
|
event.tagId(),
|
||||||
|
currentUser!.hexpubkey,
|
||||||
|
rawEvent,
|
||||||
|
);
|
||||||
|
console.log("Payment result", result);
|
||||||
|
if (result) {
|
||||||
|
setHasValidPayment(true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log("error sending zap", err);
|
||||||
|
} finally {
|
||||||
|
setCheckingPayment(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleSyncUsers() {
|
||||||
|
if (!event) return;
|
||||||
|
setSyncingUsers(true);
|
||||||
|
try {
|
||||||
|
console.log("handleSyncUsers");
|
||||||
|
await updateListUsersFromZaps(ndk!, event.tagId(), rawEvent);
|
||||||
|
toast.success("Users Synced!");
|
||||||
|
} catch (err) {
|
||||||
|
console.log("error syncing users", err);
|
||||||
|
} finally {
|
||||||
|
setSyncingUsers(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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="space-y-1 @sm:space-y-2">
|
||||||
|
<h2 className="font-condensed text-2xl font-semibold sm:text-3xl lg:text-4xl">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ProfileInfo pubkey={pubkey} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{!!currentUser && currentUser.pubkey === pubkey && (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => modal?.show(<CreateEventModal />)}>
|
||||||
|
Add Event
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={"outline"}
|
||||||
|
loading={syncingUsers}
|
||||||
|
onClick={() => void handleSyncUsers()}
|
||||||
|
>
|
||||||
|
Sync users
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() =>
|
||||||
|
modal?.show(<EditListModal listEvent={rawEvent} />)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{subscriptionsEnabled && !isMember && <Button>Subscribe</Button>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-1 @md:pt-2">
|
||||||
|
{!!description && (
|
||||||
|
<p className="line-clamp-3 text-sm text-muted-foreground md:text-sm">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -13,18 +13,8 @@ import Spinner from "@/components/spinner";
|
|||||||
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
|
||||||
import ProfileInfo from "./_components/ProfileInfo";
|
import ProfileInfo from "./_components/ProfileInfo";
|
||||||
import Feed from "@/containers/Feed";
|
import Feed from "@/containers/Feed";
|
||||||
|
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||||
const demo = [
|
import Header from "./_components/Header";
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
title: "BTC Radio",
|
|
||||||
description:
|
|
||||||
"BTC Radio is the best fuking show ever. you should sub to it. now",
|
|
||||||
picture:
|
|
||||||
"https://assets.whop.com/cdn-cgi/image/width=1080/https://assets.whop.com/images/images/51602.original.png?1693358530",
|
|
||||||
tags: ["music", "crypto", "art"],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ListPage({
|
export default function ListPage({
|
||||||
params: { naddr },
|
params: { naddr },
|
||||||
@ -33,14 +23,11 @@ export default function ListPage({
|
|||||||
naddr: string;
|
naddr: string;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const [activeTab, setActiveTab] = useState("feed");
|
|
||||||
const { type, data } = nip19.decode(naddr);
|
const { type, data } = nip19.decode(naddr);
|
||||||
console.log("PASSED", naddr, data);
|
|
||||||
if (type !== "naddr") {
|
if (type !== "naddr") {
|
||||||
throw new Error("Invalid list");
|
throw new Error("Invalid list");
|
||||||
}
|
}
|
||||||
const { identifier, kind, pubkey } = data;
|
const { identifier, kind, pubkey } = data;
|
||||||
const { profile } = useProfile(pubkey);
|
|
||||||
const { events } = useEvents({
|
const { events } = useEvents({
|
||||||
filter: {
|
filter: {
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
@ -60,56 +47,10 @@ export default function ListPage({
|
|||||||
}
|
}
|
||||||
const noteIds = getTagsValues("e", event.tags).filter(Boolean);
|
const noteIds = getTagsValues("e", event.tags).filter(Boolean);
|
||||||
console.log("notes", event.tags);
|
console.log("notes", event.tags);
|
||||||
const title =
|
|
||||||
getTagValues("title", event.tags) ??
|
|
||||||
getTagValues("name", event.tags) ??
|
|
||||||
"Untitled";
|
|
||||||
const image =
|
|
||||||
getTagValues("image", event.tags) ??
|
|
||||||
getTagValues("picture", event.tags) ??
|
|
||||||
getTagValues("banner", event.tags) ??
|
|
||||||
profile?.banner;
|
|
||||||
|
|
||||||
const description = getTagValues("description", event.tags);
|
|
||||||
return (
|
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 sm:p-4">
|
||||||
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
|
<Header naddr={naddr} />
|
||||||
<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 justify-between gap-x-1.5 @lg:gap-x-2.5">
|
|
||||||
<div className="space-y-1 @sm:space-y-2">
|
|
||||||
<h2 className="font-condensed text-2xl font-semibold sm:text-3xl lg:text-4xl">
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<ProfileInfo pubkey={pubkey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button>Subscribe</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-1 @md:pt-2">
|
|
||||||
{!!description && (
|
|
||||||
<p className="line-clamp-3 text-sm text-muted-foreground md:text-sm">
|
|
||||||
{description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
|
<div className="relative overflow-hidden rounded-[1rem] border bg-muted p-[0.5rem] @container">
|
||||||
<div className="space-y-3 overflow-hidden rounded-[0.5rem] p-0">
|
<div className="space-y-3 overflow-hidden rounded-[0.5rem] p-0">
|
||||||
<Feed
|
<Feed
|
||||||
|
@ -12,7 +12,7 @@ import { createEvent } from "@/lib/actions/create";
|
|||||||
import { getTagValues } from "@/lib/nostr/utils";
|
import { getTagValues } from "@/lib/nostr/utils";
|
||||||
import { NDKList } from "@nostr-dev-kit/ndk";
|
import { NDKList } from "@nostr-dev-kit/ndk";
|
||||||
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
|
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
const CreateListSchema = z.object({
|
const CreateListSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
@ -26,6 +26,7 @@ type CreateListType = z.infer<typeof CreateListSchema>;
|
|||||||
export default function CreateList() {
|
export default function CreateList() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { currentUser, updateUser } = useCurrentUser();
|
const { currentUser, updateUser } = useCurrentUser();
|
||||||
const { ndk } = useNDK();
|
const { ndk } = useNDK();
|
||||||
@ -80,8 +81,13 @@ export default function CreateList() {
|
|||||||
}
|
}
|
||||||
// getLists(currentUser!.hexpubkey);
|
// getLists(currentUser!.hexpubkey);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
toast.success("List Created!");
|
if (event) {
|
||||||
modal?.hide();
|
toast.success("List Created!");
|
||||||
|
modal?.hide();
|
||||||
|
router.push(`/list/${event.encode()}`);
|
||||||
|
} else {
|
||||||
|
toast.error("An error occured");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<FormModal
|
<FormModal
|
||||||
|
94
components/Modals/EditList.tsx
Normal file
94
components/Modals/EditList.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import FormModal from "./FormModal";
|
||||||
|
import { z } from "zod";
|
||||||
|
import useEvents from "@/lib/hooks/useEvents";
|
||||||
|
import { updateList } from "@/lib/actions/create";
|
||||||
|
import { unixTimeNowInSeconds } from "@/lib/nostr/dates";
|
||||||
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
import { NostrEvent } from "@nostr-dev-kit/ndk";
|
||||||
|
import { getTagValues } from "@/lib/nostr/utils";
|
||||||
|
|
||||||
|
const EditListSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type EditListType = z.infer<typeof EditListSchema>;
|
||||||
|
|
||||||
|
type EditListModalProps = {
|
||||||
|
listEvent: NostrEvent;
|
||||||
|
};
|
||||||
|
export default function EditListModal({ listEvent }: EditListModalProps) {
|
||||||
|
const modal = useModal();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const { events } = useEvents({
|
||||||
|
filter: {
|
||||||
|
kinds: [listEvent.kind as number],
|
||||||
|
authors: [listEvent.pubkey],
|
||||||
|
since: unixTimeNowInSeconds() - 10,
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
enabled: sent,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (events.length) {
|
||||||
|
console.log("Done!");
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.success("List Updated!");
|
||||||
|
modal?.hide();
|
||||||
|
}
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
|
async function handleSubmit(listData: EditListType) {
|
||||||
|
setIsLoading(true);
|
||||||
|
const newTags = Object.entries(listData);
|
||||||
|
setSent(true);
|
||||||
|
const result = await updateList(ndk!, listEvent, newTags);
|
||||||
|
}
|
||||||
|
const defaultValues: Partial<EditListType> = {
|
||||||
|
title:
|
||||||
|
getTagValues("title", listEvent.tags) ??
|
||||||
|
getTagValues("name", listEvent.tags),
|
||||||
|
image:
|
||||||
|
getTagValues("image", listEvent.tags) ??
|
||||||
|
getTagValues("picture", listEvent.tags),
|
||||||
|
description:
|
||||||
|
getTagValues("description", listEvent.tags) ??
|
||||||
|
getTagValues("summary", listEvent.tags),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
title="Edit List"
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
label: "Title",
|
||||||
|
type: "input",
|
||||||
|
slug: "title",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Image",
|
||||||
|
type: "input",
|
||||||
|
slug: "image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Description",
|
||||||
|
type: "text-area",
|
||||||
|
slug: "description",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
defaultValues={defaultValues ?? {}}
|
||||||
|
formSchema={EditListSchema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
isSubmitting={isLoading}
|
||||||
|
cta={{
|
||||||
|
text: "Save Changes",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user