live streams

This commit is contained in:
zmeyer44 2023-10-16 10:59:28 -04:00
parent d98fad2844
commit 919b768a39
17 changed files with 423 additions and 45 deletions

View File

@ -1,3 +1,4 @@
"use client";
import {
Section,
SectionHeader,
@ -7,9 +8,28 @@ import {
import LiveBadge from "@/components/Badges/LiveBadge";
import { Button } from "@/components/ui/button";
import { RiArrowRightLine } from "react-icons/ri";
import VideoCard from "@/components/VideoCard";
import VideoCard, { VideoCardLoading } from "@/components/VideoCard";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import Link from "next/link";
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 { NDKKind } from "@nostr-dev-kit/ndk";
import { uniqBy } from "ramda";
export default function LiveStreamingSection() {
const { events } = useEvents({
filter: {
kinds: [30311 as NDKKind],
// authors: NOTABLE_ACCOUNTS.map((a) => nip19.decode(a).data.toString()),
limit: 5,
},
});
const processedEvents = uniqBy((e) => getTagValues("title", e.tags), events);
const demo = [
{
id: 1,
@ -54,13 +74,52 @@ export default function LiveStreamingSection() {
<SectionContent className="relative">
<ScrollArea>
<div className="flex space-x-2 pb-4 max-sm:px-5">
{demo.map((item) => (
<VideoCard
key={item.id}
card={item}
className="min-w-[250px] max-w-[350px]"
/>
))}
{processedEvents?.length > 3 ? (
processedEvents
.filter((e) => !!getTagValues("summary", e.tags))
.slice(0, 6)
.map((e, idx) => {
if (idx > 6) return null;
const event = e.rawEvent() as Event;
const image = getTagValues("image", event.tags) as string;
const title = getTagValues("title", event.tags) as string;
const starts = getTagValues("starts", event.tags) as string;
const tags = getTagsValues("t", event.tags) as string[];
const total_participants = getTagValues(
"total_participants",
event.tags,
) as string;
const status = getTagValues("status", event.tags) as
| "live"
| "planned"
| "ended";
return (
<Link key={e.id} href={`/article/${e.encode()}`}>
<VideoCard
card={{
image,
tags,
title,
starts: parseInt(starts),
total_participants: total_participants
? parseInt(total_participants)
: undefined,
status,
}}
className="min-w-[250px] max-w-[350px]"
/>
</Link>
);
})
) : (
<>
<VideoCardLoading className="min-w-[250px] max-w-[350px]" />
<VideoCardLoading className="min-w-[250px] max-w-[350px]" />
<VideoCardLoading className="min-w-[250px] max-w-[350px]" />
<VideoCardLoading className="min-w-[250px] max-w-[350px]" />
</>
)}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>

View File

@ -0,0 +1,36 @@
"use client";
import { useEffect } from "react";
import Article from "@/containers/Article";
import { useNDK } from "@nostr-dev-kit/ndk-react";
import { nip19 } from "nostr-tools";
import Spinner from "@/components/spinner";
import useEvents from "@/lib/hooks/useEvents";
export default function ArticlePage({
params: { key },
}: {
params: {
key: string;
};
}) {
const { ndk } = useNDK();
const { data, type } = nip19.decode(key);
const { events } = useEvents({
filter:
type === "note"
? {
ids: [data.toString()],
limit: 1,
}
: {},
});
if (events?.[0]) {
return <div className="center pt-20 text-primary">{events[0].id}</div>;
}
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}

View File

@ -6,14 +6,14 @@ import { nip19 } from "nostr-tools";
import Spinner from "@/components/spinner";
import useEvents from "@/lib/hooks/useEvents";
export default function ArticlePage({
params: { naddr },
params: { key },
}: {
params: {
naddr: string;
key: string;
};
}) {
const { ndk } = useNDK();
const { data, type } = nip19.decode(naddr);
const { data, type } = nip19.decode(key);
const { events } = useEvents({
filter:
type === "naddr"

View File

@ -0,0 +1,54 @@
"use client";
import { ReactElement, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { RiCloseFill } from "react-icons/ri";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import { useRouter } from "next/navigation";
import { getTagAllValues, getTagValues } from "@/lib/nostr/utils";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
import { getNameToShow, getTwoLetters } from "@/lib/utils";
export default function Layout(props: {
children: ReactElement;
params: { key: string };
}) {
const router = useRouter();
const { data, type } = nip19.decode(props.params.key);
const pubkey = type === "nevent" ? data.author ?? "" : "";
const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey);
return (
<div className="relative @container">
<div className="sticky inset-x-0 top-0 z-10 flex items-center justify-between border-b bg-background pb-4 pt-4">
<div className="center gap-x-3">
<Avatar className="center h-8 w-8 overflow-hidden rounded-sm bg-muted">
<AvatarImage src={profile?.image} alt="user" />
<AvatarFallback className="text-xs">
{getTwoLetters({ profile, npub })}
</AvatarFallback>
</Avatar>
<span className="text-xs uppercase text-muted-foreground">
{getNameToShow({ profile, npub })}
</span>
</div>
<Button
onClick={() => {
if (sessionStorage.getItem("RichHistory")) {
void router.back();
} else {
void router.push("/app");
}
}}
size="icon"
variant={"outline"}
className=""
>
<RiCloseFill className="h-5 w-5" />
</Button>
</div>
<div className="h-[20px] w-full"></div>
<div className="mx-auto max-w-2xl">{props.children}</div>
</div>
);
}

View File

@ -0,0 +1,42 @@
"use client";
import { useEffect } from "react";
import Article from "@/containers/Article";
import { useNDK } from "@nostr-dev-kit/ndk-react";
import { nip19, type Event } from "nostr-tools";
import Spinner from "@/components/spinner";
import useEvents from "@/lib/hooks/useEvents";
import KindCard from "@/components/KindCard";
export default function EventPage({
params: { key },
}: {
params: {
key: string;
};
}) {
const { ndk } = useNDK();
const { data, type } = nip19.decode(key);
const { events } = useEvents({
filter:
type === "nevent"
? {
ids: [data.id],
limit: 1,
}
: {},
});
if (events?.[0]) {
const event = events[0].rawEvent() as Event;
return (
<div className="center pt-7 text-primary">
<KindCard {...event} />
</div>
);
}
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}

View File

@ -0,0 +1,54 @@
"use client";
import { ReactElement, ReactNode } from "react";
import { Button } from "@/components/ui/button";
import { RiCloseFill } from "react-icons/ri";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
import { useRouter } from "next/navigation";
import { getTagAllValues, getTagValues } from "@/lib/nostr/utils";
import useProfile from "@/lib/hooks/useProfile";
import { nip19 } from "nostr-tools";
import { getNameToShow, getTwoLetters } from "@/lib/utils";
export default function Layout(props: {
children: ReactElement;
params: { key: string };
}) {
const router = useRouter();
const { data, type } = nip19.decode(props.params.key);
const pubkey = type === "nevent" ? data.author ?? "" : "";
const { profile } = useProfile(pubkey);
const npub = nip19.npubEncode(pubkey);
return (
<div className="relative @container">
<div className="sticky inset-x-0 top-0 z-10 flex items-center justify-between border-b bg-background pb-4 pt-4">
<div className="center gap-x-3">
<Avatar className="center h-8 w-8 overflow-hidden rounded-sm bg-muted">
<AvatarImage src={profile?.image} alt="user" />
<AvatarFallback className="text-xs">
{getTwoLetters({ profile, npub })}
</AvatarFallback>
</Avatar>
<span className="text-xs uppercase text-muted-foreground">
{getNameToShow({ profile, npub })}
</span>
</div>
<Button
onClick={() => {
if (sessionStorage.getItem("RichHistory")) {
void router.back();
} else {
void router.push("/app");
}
}}
size="icon"
variant={"outline"}
className=""
>
<RiCloseFill className="h-5 w-5" />
</Button>
</div>
<div className="h-[20px] w-full"></div>
<div className="mx-auto max-w-2xl">{props.children}</div>
</div>
);
}

View File

@ -0,0 +1,42 @@
"use client";
import { useEffect } from "react";
import Article from "@/containers/Article";
import { useNDK } from "@nostr-dev-kit/ndk-react";
import { nip19, type Event } from "nostr-tools";
import Spinner from "@/components/spinner";
import useEvents from "@/lib/hooks/useEvents";
import KindCard from "@/components/KindCard";
export default function EventPage({
params: { key },
}: {
params: {
key: string;
};
}) {
const { ndk } = useNDK();
const { data, type } = nip19.decode(key);
const { events } = useEvents({
filter:
type === "nevent"
? {
ids: [data.id],
limit: 1,
}
: {},
});
if (events?.[0]) {
const event = events[0].rawEvent() as Event;
return (
<div className="center pt-7 text-primary">
<KindCard {...event} />
</div>
);
}
return (
<div className="center pt-20 text-primary">
<Spinner />
</div>
);
}

View File

@ -0,0 +1,37 @@
import { ReactElement, ReactNode } from "react";
import { nip19 } from "nostr-tools";
import { redirect } from "next/navigation";
export default function ModalLayout(props: {
children: ReactElement;
"1": ReactNode;
"30023": ReactNode;
event: ReactNode;
params: {
key: string;
};
}) {
const key = props.params.key;
const { data, type } = nip19.decode(key);
if (type === "naddr") {
return (
<div className="z-overlay fixed inset-y-[10px] left-[10px] right-[10px] overflow-hidden overflow-y-auto rounded-lg border bg-background px-4 sm:left-[calc(10px_+_var(--sidebar-closed-width))] xl:left-[calc(10px_+_var(--sidebar-open-width))]">
{props[30023]}
</div>
);
} else if (type === "note") {
return (
<div className="z-overlay fixed inset-y-[10px] left-[10px] right-[10px] overflow-hidden overflow-y-auto rounded-lg border bg-background px-4 sm:left-[calc(10px_+_var(--sidebar-closed-width))] xl:left-[calc(10px_+_var(--sidebar-open-width))]">
{props[1]}
</div>
);
} else if (type === "nevent") {
return (
<div className="z-overlay fixed inset-y-[10px] left-[10px] right-[10px] overflow-hidden overflow-y-auto rounded-lg border bg-background px-4 sm:left-[calc(10px_+_var(--sidebar-closed-width))] xl:left-[calc(10px_+_var(--sidebar-open-width))]">
{props.event}
</div>
);
} else if (type === "npub") {
return redirect(`/${key}`);
}
return redirect(`/app`);
}

View File

@ -1,9 +0,0 @@
import { ReactElement } from "react";
export default function ModalLayout({ children }: { children: ReactElement }) {
return (
<div className="z-overlay fixed inset-y-[10px] left-[10px] right-[10px] overflow-hidden overflow-y-auto rounded-lg border bg-background px-4 sm:left-[calc(10px_+_var(--sidebar-closed-width))] xl:left-[calc(10px_+_var(--sidebar-open-width))]">
{children}
</div>
);
}

BIN
bun.lockb

Binary file not shown.

View File

@ -33,7 +33,7 @@ export default function Kind1(props: Event) {
},
]}
>
<CardDescription className="text-base text-foreground">
<CardDescription className="text-sm font-normal text-secondary-foreground">
<RenderText text={content} />
</CardDescription>
{!!r.length && (

View File

@ -39,12 +39,13 @@ export default function Container({
actionOptions = [],
}: CreatorCardProps) {
return (
<Card className="relative flex h-full flex-col overflow-hidden">
<Card className="relative flex h-full w-full flex-col overflow-hidden @container">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-4 pb-4">
{pubkey ? <ProfileHeader pubkey={pubkey} /> : <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")}
{!!createdAt &&
formatDate(new Date(createdAt * 1000), "MMM Do, h:m a")}
<DropDownMenu options={actionOptions}>
<Button
size={"sm"}
@ -60,11 +61,13 @@ export default function Container({
{children}
<div className="mt-auto">
{!!contentTags?.length && (
<div className="-mb-1 mt-1 max-h-[52px] overflow-hidden">
<div className="mb-2 mt-1 max-h-[52px] overflow-hidden">
<Tags tags={contentTags} />
</div>
)}
<Actions />
<div className="border-t">
<Actions />
</div>
</div>
</CardContent>
</Card>

View File

@ -14,7 +14,7 @@ export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
const npub = nip19.npubEncode(pubkey);
return (
<Link href={`/${npub}`} className="center group gap-x-3">
<Avatar className="center h-8 w-8 overflow-hidden rounded-sm bg-muted">
<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 })}
@ -55,7 +55,7 @@ export default function ProfileHeader({ pubkey }: ProfileHeaderProps) {
export function LoadingProfileHeader() {
return (
<div className="center group gap-x-3">
<Avatar className="center h-8 w-8 overflow-hidden rounded-sm bg-muted"></Avatar>
<Avatar className="center h-9 w-9 overflow-hidden rounded-sm bg-muted @md:h-10 @md:w-10"></Avatar>
<div className="space-y-1">
<Skeleton className="h-2.5 w-[70px] bg-muted" />
<Skeleton className="h-2.5 w-[100px] bg-muted" />

View File

@ -4,9 +4,14 @@ import { type Event } from "nostr-tools";
import { nip19 } from "nostr-tools";
import { toast } from "sonner";
import { copyText } from "@/lib/utils";
import { RenderText } from "../TextRendering";
import { getTagsValues } from "@/lib/nostr/utils";
import LinkCard from "@/components/LinkCard";
export default function KindDefault(props: Event) {
const { pubkey, created_at: createdAt } = props;
const { pubkey, created_at: createdAt, tags } = props;
const r = getTagsValues("r", tags).filter(Boolean);
const npub = nip19.npubEncode(pubkey);
return (
@ -29,14 +34,16 @@ export default function KindDefault(props: Event) {
},
]}
>
<CardTitle className="mb-1.5 line-clamp-2 text-lg font-semibold">
The start of the Nostr revolution
</CardTitle>
<CardDescription className="line-clamp-4 text-sm">
This is the summary of this artilce. Let's hope that it is a good
article and that it will end up being worth reading. I don't want to
waste my time on some random other stuff.
<CardDescription className="text-sm font-normal text-secondary-foreground">
<RenderText text={props.content} />
</CardDescription>
{!!r.length && (
<div className="mt-1.5 flex flex-wrap">
{r.map((url, idx) => (
<LinkCard key={idx} url={url} className="max-w-[250px]" />
))}
</div>
)}
</Container>
);
}

View File

@ -1,29 +1,37 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
import { cn, formatNumber } 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 { formatDate } from "@/lib/utils/dates";
type VideoCardProps = {
card: {
picture: string;
image: string;
title: string;
tags: string[];
starts?: number;
status: "live" | "planned" | "ended";
["total_participants"]?: number;
};
className?: string;
};
export default function VideoCard({ className, card }: VideoCardProps) {
const startTime = card?.starts ? new Date(card.starts * 1000) : null;
return (
<div
className={cn(
"group flex flex-col space-y-3 rounded-[16px] p-2 hover:bg-muted",
"group flex h-full flex-col space-y-3 rounded-[16px] p-2 hover:bg-muted",
className,
)}
>
<div className="overflow-hidden rounded-md">
<div className="relative overflow-hidden rounded-md">
<AspectRatio ratio={16 / 9} className="bg-muted">
<Image
src={card.picture}
src={card.image}
alt={card.title}
width={250}
height={150}
@ -34,17 +42,32 @@ export default function VideoCard({ className, card }: VideoCardProps) {
)}
/>
</AspectRatio>
{card.status === "live" && (
<div className="pointer-events-none absolute bottom-0 right-0 p-2">
<Badge variant={"red"}>LIVE</Badge>
</div>
)}
</div>
<div className="flex-1 space-y-2 text-base">
<h3 className="line-clamp-2 font-medium leading-none">{card.title}</h3>
<div className="flex flex-col items-start">
<div className="center gap-x-1 text-xs text-muted-foreground">
<RxClock className="h-4 w-4 text-primary" />
<span>12:00 PM</span>
<div className="flex items-center gap-x-3">
<div className="flex flex-col items-start">
{startTime && (
<div className="center gap-x-1 text-xs text-muted-foreground">
<RxClock className="h-4 w-4 text-primary" />
<span>{formatDate(new Date(startTime), "h:m a")}</span>
</div>
)}
{card["total_participants"] && (
<div className="center gap-x-1 text-xs text-muted-foreground">
<HiOutlineUsers className="h-4 w-4 text-primary" />
<span>{formatNumber(card["total_participants"])}</span>
</div>
)}
</div>
</div>
</div>
<div className="-mt-1 flex flex-wrap gap-2 overflow-x-hidden">
<div className="-mt-1 flex flex-wrap-reverse gap-2 overflow-x-hidden">
{card.tags.slice(0, 4).map((tag) => (
<Badge key={tag}>{tag}</Badge>
))}
@ -52,3 +75,31 @@ export default function VideoCard({ className, card }: VideoCardProps) {
</div>
);
}
export function VideoCardLoading({ className }: { className: string }) {
return (
<div
className={cn(
"group pointer-events-none flex flex-col space-y-3 rounded-[16px] p-2",
className,
)}
>
<div className="overflow-hidden rounded-md">
<AspectRatio ratio={16 / 9} className="bg-muted"></AspectRatio>
</div>
<div className="flex-1 space-y-2 text-base">
<Skeleton className="mb-2 h-4 w-1/3 bg-muted" />
<div className="flex flex-col items-start">
<div className="center gap-x-1 text-xs text-muted-foreground">
<RxClock className="h-4 w-4 text-primary" />
<Skeleton className="h-3 w-[50px] bg-muted" />
</div>
</div>
</div>
<div className="-mt-1 flex flex-wrap gap-2 overflow-x-hidden">
<Skeleton className="h-2 w-[50px] bg-muted" />
<Skeleton className="h-2 w-[40px] bg-muted" />
<Skeleton className="h-2 w-[30px] bg-muted" />
</div>
</div>
);
}

View File

@ -14,6 +14,7 @@ const badgeVariants = cva(
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
green:
"border-transparent bg-green-100 text-green-700 hover:bg-green-200/80",
red: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
outline: "text-foreground",

View File

@ -43,6 +43,7 @@
"react-dom": "^18",
"react-hook-form": "^7.47.0",
"react-icons": "^4.11.0",
"react-player": "^2.13.0",
"sonner": "^1.0.3",
"tailwind-merge": "^1.14.0",
"tailwind-scrollbar": "^3.0.5",