better lists

This commit is contained in:
zmeyer44 2023-10-16 13:01:46 -04:00
parent 4edede47e6
commit de586251a7
10 changed files with 234 additions and 97 deletions

View File

@ -1,4 +1,5 @@
"use client"; "use client";
import Link from "next/link";
import { import {
Section, Section,
SectionHeader, SectionHeader,
@ -15,108 +16,65 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import Image from "next/image"; import Image from "next/image";
import { cn } from "@/lib/utils"; import { cn, formatNumber, getTwoLetters } from "@/lib/utils";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { AspectRatio } from "@/components/ui/aspect-ratio"; import { AspectRatio } from "@/components/ui/aspect-ratio";
import useEvents from "@/lib/hooks/useEvents";
import { Event } from "nostr-tools";
import KindLoading from "@/components/KindCard/loading";
import { nip19 } from "nostr-tools";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import { NOTABLE_ACCOUNTS } from "@/constants";
import { type NDKKind } from "@nostr-dev-kit/ndk";
import { uniqBy } from "ramda";
import useProfile from "@/lib/hooks/useProfile";
import ListCard from "@/components/ListCard";
export default function FeaturedLists() { export default function FeaturedLists() {
const demo = [ const { events } = useEvents({
{ filter: {
id: 1, kinds: [30001 as NDKKind],
title: "BTC Radio", authors: NOTABLE_ACCOUNTS.map((a) => nip19.decode(a).data.toString()),
picture: limit: 50,
"https://assets.whop.com/cdn-cgi/image/width=1080/https://assets.whop.com/images/images/51602.original.png?1693358530",
tags: ["music", "crypto", "art"],
}, },
{ });
id: 2,
title: "The Book of Alpha: NFTs and crypto taking over. Market Talk", const processedEvents = events
picture: .sort((a, b) => {
"https://assets.whop.com/cdn-cgi/image/width=1080/https://assets.whop.com/images/images/31095.thumbnail.png?1692203850", const aTitle =
tags: ["NFTs", "crypto", "art", "trading"], getTagValues("title", a.tags) ??
}, getTagValues("description", a.tags) ??
{ a.content;
id: 3, const bTitle =
title: "Space Talk: What's Elon up to?", getTagValues("title", b.tags) ??
picture: getTagValues("description", b.tags) ??
"https://assets.whop.com/cdn-cgi/image/width=1080/https://assets.whop.com/images/images/40088.original.png?1692206315", b.content;
tags: ["Space"], const aTitleLength = aTitle?.split(" ").length ?? 0;
}, const bTitleLength = bTitle?.split(" ").length ?? 0;
{ if (aTitleLength && bTitleLength) {
id: 4, if (aTitleLength < bTitleLength) {
title: "The Book of Alpha: NFTs and crypto taking over. Market Talk", return 1;
picture: } else return -1;
"https://assets.whop.com/cdn-cgi/image/width=1080/https://assets.whop.com/images/images/40680.original.png?1692206434", }
tags: ["Market"], if (bTitleLength) return 1;
}, return -1;
]; })
.slice(0, 6);
return ( return (
<Section> <Section>
<SectionHeader> <SectionHeader>
<div className="center gap-x-2"> <div className="center gap-x-2">
<SectionTitle>Featured Lists</SectionTitle> <SectionTitle>Featured Lists</SectionTitle>
{processedEvents.length}
</div> </div>
<Button variant={"ghost"}> <Button variant={"ghost"}>
View all <RiArrowRightLine className="ml-1 h-4 w-4" /> View all <RiArrowRightLine className="ml-1 h-4 w-4" />
</Button> </Button>
</SectionHeader> </SectionHeader>
<SectionContent className="sm:md-feed-cols relative flex flex-col gap-3"> <SectionContent className="sm:md-feed-cols relative flex flex-col gap-3">
{demo.map((e) => ( {processedEvents.map((e) => (
<Card key={e.id} className="max-sm:border-0 max-sm:shadow-none"> <ListCard key={e.id} event={e} />
<div className="hidden overflow-hidden rounded-t-md sm:flex">
<AspectRatio ratio={16 / 9} className="bg-muted">
<Image
width={250}
height={150}
src={e.picture}
alt={e.title}
unoptimized
className={cn(
"h-auto w-auto object-cover transition-all group-hover:scale-105",
"aspect-video",
)}
/>
</AspectRatio>
</div>
<CardContent className="flex gap-x-3 p-0 sm:p-3">
<div className="shrink-0">
<Avatar className="center h-[60px] w-[60px] overflow-hidden rounded-sm bg-muted sm:h-12 sm:w-12">
<AvatarImage
src={e.picture}
alt="user"
className="h-full w-auto max-w-none object-cover"
/>
<AvatarFallback className="text-sm">SC</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-1 justify-between gap-3 max-sm:items-center">
<div className="flex flex-1 flex-col justify-between">
<div className="">
<CardTitle className="line-clamp-1 max-sm:text-sm">
{e.title}
</CardTitle>
<CardDescription className="line-clamp-2 text-xs">
Here is my description of this list that I am offering
</CardDescription>
</div>
<div className="max-sm:hidden">
<Badge variant={"outline"}>100 subs</Badge>
</div>
</div>
<div className="gap-y-1.5 text-right">
<div className="max-sm:hidden">
<div className="text-sm font-medium">2k sats</div>
<div className="text-[10px] text-muted-foreground">
/month
</div>
</div>
<Badge variant={"green"} className="">
Free
</Badge>
</div>
</div>
</CardContent>
</Card>
))} ))}
</SectionContent> </SectionContent>
</Section> </Section>

View File

@ -17,7 +17,7 @@ import KindLoading from "@/components/KindCard/loading";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils"; import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import { NOTABLE_ACCOUNTS } from "@/constants"; import { NOTABLE_ACCOUNTS } from "@/constants";
import { NDKKind } from "@nostr-dev-kit/ndk"; import { type NDKKind } from "@nostr-dev-kit/ndk";
import { uniqBy } from "ramda"; import { uniqBy } from "ramda";
export default function LiveStreamingSection() { export default function LiveStreamingSection() {
@ -67,7 +67,6 @@ export default function LiveStreamingSection() {
.filter((e) => !!getTagValues("summary", e.tags)) .filter((e) => !!getTagValues("summary", e.tags))
.slice(0, 6) .slice(0, 6)
.map((e, idx) => { .map((e, idx) => {
if (idx > 6) return null;
const event = e.rawEvent() as Event; const event = e.rawEvent() as Event;
const image = getTagValues("image", event.tags) as string; const image = getTagValues("image", event.tags) as string;
const title = getTagValues("title", event.tags) as string; const title = getTagValues("title", event.tags) as string;

View File

@ -15,7 +15,7 @@ export default function Layout(props: {
}) { }) {
const router = useRouter(); const router = useRouter();
const { data, type } = nip19.decode(props.params.key); const { data, type } = nip19.decode(props.params.key);
const pubkey = type === "nevent" ? data.author ?? "" : ""; const pubkey = type === "naddr" ? data.pubkey ?? "" : "";
const { profile } = useProfile(pubkey); const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey); const npub = nip19.npubEncode(pubkey);
return ( return (

View File

@ -56,7 +56,7 @@ export default function CreatorCard({
profile?.picture ?? profile?.picture ??
`https://bitcoinfaces.xyz/api/get-image?name=${npub}&onchain=false` `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 object-cover transition-all duration-300 group-hover:left-[50px] group-hover:top-[65px] group-hover:w-[70px]" 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} height={100}
width={100} width={100}
unoptimized unoptimized

View File

@ -7,7 +7,6 @@ import { toast } from "sonner";
import { copyText } from "@/lib/utils"; import { copyText } from "@/lib/utils";
import { RenderText } from "../TextRendering"; import { RenderText } from "../TextRendering";
import { getTagValues, getTagsValues } from "@/lib/nostr/utils"; import { getTagValues, getTagsValues } from "@/lib/nostr/utils";
import LinkCard from "@/components/LinkCard";
import ReactPlayer from "react-player"; import ReactPlayer from "react-player";
export default function Kind30311(props: Event) { export default function Kind30311(props: Event) {
@ -40,7 +39,12 @@ export default function Kind30311(props: Event) {
}, },
]} ]}
> >
<ReactPlayer url={streamingUrl} muted={false} controls={true} /> <ReactPlayer
url={streamingUrl}
playing={true}
muted={false}
controls={true}
/>
<div className="border-t pt-4"> <div className="border-t pt-4">
{!!title && <CardTitle className="text-base">{title}</CardTitle>} {!!title && <CardTitle className="text-base">{title}</CardTitle>}
{!!summary && <CardDescription>{summary}</CardDescription>} {!!summary && <CardDescription>{summary}</CardDescription>}

View File

@ -45,7 +45,7 @@ export default function Container({
<div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground"> <div className="-mr-1 flex items-center gap-x-1.5 text-xs text-muted-foreground">
{!!createdAt && {!!createdAt &&
formatDate(new Date(createdAt * 1000), "MMM Do, h:m a")} formatDate(new Date(createdAt * 1000), "MMM Do, h:mm a")}
<DropDownMenu options={actionOptions}> <DropDownMenu options={actionOptions}>
<Button <Button
size={"sm"} size={"sm"}

View File

@ -0,0 +1,154 @@
import Image from "next/image";
import { cn, formatNumber, getTwoLetters, formatCount } from "@/lib/utils";
import { Badge } from "../ui/badge";
import { RxClock } from "react-icons/rx";
import { HiOutlineUsers } from "react-icons/hi";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Skeleton } from "@/components/ui/skeleton";
import { type Event } from "nostr-tools";
import { formatDate } from "@/lib/utils/dates";
import { getTagValues, getTagsValues, getPrice } from "@/lib/nostr/utils";
import { nip19 } from "nostr-tools";
import useProfile from "@/lib/hooks/useProfile";
import { type NDKEvent } from "@nostr-dev-kit/ndk";
import {
Card,
CardContent,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
type ListCardProps = {
event: NDKEvent;
className?: string;
};
export default function ListCard({ className, event: e }: ListCardProps) {
const event = e.rawEvent() as Event;
const image = getTagValues("image", event.tags) as string;
const title =
getTagValues("title", event.tags) ?? getTagValues("name", event.tags);
const description = getTagValues("description", event.tags);
const userCount = getTagsValues("p", event.tags).length;
const price = getPrice(event.tags);
const tags = getTagsValues("t", event.tags) as string[];
const { profile } = useProfile(e.pubkey);
const npub = nip19.npubEncode(e.pubkey);
return (
<Card
onClick={() => console.log(event.tags)}
className="max-sm:border-0 max-sm:shadow-none"
>
<div className="hidden overflow-hidden rounded-t-md sm:flex">
<AspectRatio ratio={16 / 9} className="bg-muted">
<Image
width={250}
height={150}
src={image ?? profile?.banner}
alt={title ?? ""}
unoptimized
className={cn(
"h-auto w-auto object-cover transition-all group-hover:scale-105",
"aspect-video",
)}
/>
</AspectRatio>
</div>
<CardContent className="flex gap-x-3 p-0 sm:p-3">
<div className="shrink-0">
<Avatar className="center h-[60px] w-[60px] overflow-hidden rounded-sm bg-muted sm:h-12 sm:w-12">
<AvatarImage
src={profile?.image}
alt="user"
className="h-full w-auto max-w-none object-cover"
/>
<AvatarFallback className="text-sm">
{getTwoLetters({ profile, npub })}
</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-1 justify-between gap-3 max-sm:items-center">
<div className="flex flex-1 flex-col justify-between">
<div className="">
<CardTitle className="line-clamp-1 max-sm:text-sm">
{title}
</CardTitle>
<CardDescription className="line-clamp-2 text-xs">
{description}
</CardDescription>
</div>
<div className="max-sm:hidden">
<Badge variant={"outline"}>{`${userCount} subs`}</Badge>
</div>
</div>
<div className="gap-y-1.5 text-right">
{/* <div className="max-sm:hidden">
<div className="text-sm font-medium">2k sats</div>
<div className="text-[10px] text-muted-foreground">
/month
</div>
</div> */}
{price ? (
<>
<Badge variant={"green"} className="">
{`${formatCount(price.asSats)} sats`}
</Badge>
{!!price.frequency && (
<div className="text-[10px] text-muted-foreground">
{`per ${price.frequency}`}
</div>
)}
</>
) : (
<Badge variant={"green"} className="">
Free
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
);
}
export function ListCardLoading({ className }: { className?: string }) {
return (
<Card className="max-sm:border-0 max-sm:shadow-none">
<div className="hidden overflow-hidden rounded-t-md sm:flex">
<AspectRatio ratio={16 / 9} className="bg-muted"></AspectRatio>
</div>
<CardContent className="flex gap-x-3 p-0 sm:p-3">
<div className="shrink-0">
<Avatar className="center h-[60px] w-[60px] overflow-hidden rounded-sm bg-muted sm:h-12 sm:w-12"></Avatar>
</div>
<div className="flex flex-1 justify-between gap-3 max-sm:items-center">
<div className="flex flex-1 flex-col justify-between">
<div className="">
<CardTitle className="line-clamp-1 max-sm:text-sm">
<Skeleton className="h-4 w-2/3 bg-muted" />
</CardTitle>
<CardDescription className="line-clamp-2 text-xs">
<Skeleton className="h-2 w-1/2 bg-muted" />
<Skeleton className="h-2 w-1/3 bg-muted" />
<Skeleton className="h-2 w-2/5 bg-muted" />
</CardDescription>
</div>
<div className="max-sm:hidden">
<Skeleton className="h-2 w-[30px] bg-muted" />
</div>
</div>
<div className="gap-y-1.5 text-right">
{/* <div className="max-sm:hidden">
<div className="text-sm font-medium">2k sats</div>
<div className="text-[10px] text-muted-foreground">
/month
</div>
</div> */}
<Skeleton className="h-3 w-[30px] bg-muted" />
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -57,12 +57,12 @@ export default function VideoCard({ className, card }: VideoCardProps) {
{!!startTime && ( {!!startTime && (
<div className="center gap-x-1 text-xs text-muted-foreground"> <div className="center gap-x-1 text-xs text-muted-foreground">
<RxClock className="h-4 w-4 text-primary" /> <RxClock className="h-4 w-4 text-primary" />
<span>{formatDate(new Date(startTime), "h:m a")}</span> <span>{formatDate(new Date(startTime), "h:mm a")}</span>
{!!endTime && ( {!!endTime && (
<> <>
{" "} {" "}
<span>-</span>{" "} <span>-</span>{" "}
<span>{formatDate(new Date(endTime), "h:m a")}</span> <span>{formatDate(new Date(endTime), "h:mm a")}</span>
</> </>
)} )}
</div> </div>

View File

@ -33,6 +33,7 @@ export const NOTABLE_ACCOUNTS = [
"npub1csamkk8zu67zl9z4wkp90a462v53q775aqn5q6xzjdkxnkvcpd7srtz4x9", "npub1csamkk8zu67zl9z4wkp90a462v53q775aqn5q6xzjdkxnkvcpd7srtz4x9",
"npub1ejxswthae3nkljavznmv66p9ahp4wmj4adux525htmsrff4qym9sz2t3tv", "npub1ejxswthae3nkljavznmv66p9ahp4wmj4adux525htmsrff4qym9sz2t3tv",
"npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg", "npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg",
"npub1zach44xjpc4yyhx6pgse2cj2pf98838kja03dv2e8ly8lfr094vqvm5dy5",
]; ];
export const BANNER = export const BANNER =

View File

@ -1,5 +1,5 @@
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { btcToSats } from "../utils";
export const NOSTR_BECH32_REGEXP = export const NOSTR_BECH32_REGEXP =
/^(npub|nprofile|note|nevent|naddr|nrelay)1[023456789acdefghjklmnpqrstuvwxyz]+/; /^(npub|nprofile|note|nevent|naddr|nrelay)1[023456789acdefghjklmnpqrstuvwxyz]+/;
@ -62,3 +62,24 @@ export const getTagsAllValues = (name: string, tags: string[][]) => {
const itemTags = tags.filter((tag: string[]) => tag[0] === name); const itemTags = tags.filter((tag: string[]) => tag[0] === name);
return itemTags.map(([key, ...vals]) => vals) ?? []; return itemTags.map(([key, ...vals]) => vals) ?? [];
}; };
export const getPrice = (tags: string[][]) => {
const price = tags.find(([i]) => i === "price");
if (!price) return;
const [_, amount, currency = "BTC", frequency] = price as [
string,
string,
string | undefined,
string | undefined,
];
return {
amount,
currency,
frequency,
asSats:
currency?.toLowerCase() === "btc"
? btcToSats(parseFloat(amount))
: parseInt(amount),
};
};