live streams
This commit is contained in:
parent
d98fad2844
commit
919b768a39
@ -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>
|
||||
|
36
app/(app)/article/[key]/@1/page.tsx
Normal file
36
app/(app)/article/[key]/@1/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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"
|
54
app/(app)/article/[key]/@30311/layout.tsx
Normal file
54
app/(app)/article/[key]/@30311/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
42
app/(app)/article/[key]/@30311/page.tsx
Normal file
42
app/(app)/article/[key]/@30311/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
54
app/(app)/article/[key]/@event/layout.tsx
Normal file
54
app/(app)/article/[key]/@event/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
42
app/(app)/article/[key]/@event/page.tsx
Normal file
42
app/(app)/article/[key]/@event/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
37
app/(app)/article/[key]/layout.tsx
Normal file
37
app/(app)/article/[key]/layout.tsx
Normal 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`);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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 && (
|
||||
|
@ -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>
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user