feat: style upgrades

This commit is contained in:
2024-05-20 16:45:10 +01:00
parent 6250456435
commit 21919e1e3b
78 changed files with 1168 additions and 898 deletions

View File

@ -82,11 +82,13 @@ export default function Category() {
const results = useRequestBuilder(sub);
return (
<div>
<div className="flex gap-4 overflow-x-scroll scrollbar-hidden">
{AllCategories.map(a => (
<CategoryLink key={a.id} id={a.id} name={a.name} icon={a.icon} />
))}
<div className="px-2 p-4">
<div className="px-2 min-w-0">
<div className="flex gap-4 overflow-x-scroll scrollbar-hidden">
{AllCategories.map(a => (
<CategoryLink key={a.id} id={a.id} name={a.name} icon={a.icon} />
))}
</div>
</div>
{id && (
<div className="flex gap-4 py-8">

View File

@ -2,7 +2,7 @@ import { useParams } from "react-router-dom";
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
import { unwrap } from "@snort/shared";
import { LiveChat } from "@/element/live-chat";
import { LiveChat } from "@/element/chat/live-chat";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { findTag } from "@/utils";
import { useZapGoal } from "@/hooks/goals";
@ -21,7 +21,6 @@ export function ChatPopout() {
ev={ev}
link={lnk}
canWrite={chat}
showHeader={false}
showScrollbar={false}
goal={goal}
className="h-inherit"

View File

@ -1,4 +1,4 @@
import { ChatZap } from "@/element/live-chat";
import { ChatZap } from "@/element/chat/live-chat";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { useMemo } from "react";
@ -20,7 +20,7 @@ export function DashboardZapColumn({
const reactions = useEventReactions(link, feed);
const sortedZaps = useMemo(
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
[reactions.zaps]
[reactions.zaps],
);
const latestZap = sortedZaps.at(0);
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);

View File

@ -1,12 +1,12 @@
import { LiveChat } from "@/element/live-chat";
import LiveVideoPlayer from "@/element/live-video-player";
import { LiveChat } from "@/element/chat/live-chat";
import LiveVideoPlayer from "@/element/stream/live-video-player";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { extractStreamInfo } from "@/utils";
import { EventExt, NostrEvent, NostrLink } from "@snort/system";
import { SnortContext, useReactions } from "@snort/system-react";
import { Suspense, lazy, useContext, useEffect, useMemo, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { StreamTimer } from "@/element/stream-time";
import { StreamTimer } from "@/element/stream/stream-time";
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
import { DashboardRaidButton } from "./button-raid";
import { DashboardZapColumn } from "./column-zaps";
@ -71,7 +71,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
}
},
true
true,
);
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;

View File

@ -1,5 +1,5 @@
import { StreamState } from "@/const";
import LiveVideoPlayer from "@/element/live-video-player";
import LiveVideoPlayer from "@/element/stream/live-video-player";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useStreamLink } from "@/hooks/stream-link";
import { extractStreamInfo, trackEvent } from "@/utils";

View File

@ -1,16 +1,20 @@
import { ReactNode, createContext, useState } from "react";
import { ReactNode, createContext, useContext, useState } from "react";
interface LayoutContextType {
leftNav: boolean;
leftNavExpand: boolean;
showHeader: boolean;
theme: string;
update: (fn: (c: LayoutContextType) => LayoutContextType) => void;
}
const defaultLayoutContext: LayoutContextType = {
leftNav: true,
leftNavExpand: false,
showHeader: true,
theme: "",
update: c => c,
};
export const LayoutContext = createContext<LayoutContextType>(defaultLayoutContext);
const LayoutContext = createContext<LayoutContextType>(defaultLayoutContext);
export function LayoutContextProvider({ children }: { children: ReactNode }) {
const [value, setValue] = useState<LayoutContextType>(defaultLayoutContext);
@ -26,3 +30,7 @@ export function LayoutContextProvider({ children }: { children: ReactNode }) {
</LayoutContext.Provider>
);
}
export function useLayout() {
return useContext(LayoutContext);
}

View File

@ -11,11 +11,11 @@ import { FormattedMessage } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
import { useLang } from "@/hooks/lang";
import { useLogin } from "@/hooks/login";
import { useContext, useState } from "react";
import { useState } from "react";
import { Profile } from "@/element/profile";
import { SearchBar } from "./search";
import { NavLinkIcon } from "./nav-icon";
import { LayoutContext } from "./context";
import { useLayout } from "./context";
export function HeaderNav() {
const navigate = useNavigate();
@ -23,7 +23,7 @@ export function HeaderNav() {
const [showLogin, setShowLogin] = useState(false);
const { lang, setLang } = useLang();
const country = lang.split(/[-_]/i)[1]?.toLowerCase();
const layoutState = useContext(LayoutContext);
const layoutState = useLayout();
function langSelector() {
return (
@ -54,16 +54,24 @@ export function HeaderNav() {
if (!login) return;
return (
<div className="flex gap-3 items-center pr-4 py-1">
<div className="flex gap-2 items-center pr-4 py-1">
{(!import.meta.env.VITE_SINGLE_PUBLISHER || import.meta.env.VITE_SINGLE_PUBLISHER === login.pubkey) && (
<>
<Link to="/upload">
<IconButton iconName="upload" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
</Link>
<Link to="/dashboard">
<IconButton iconName="signal" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
</Link>
</>
<Menu
menuClassName="ctx-menu"
menuButton={
<IconButton iconName="plus-circle" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
}
align="end"
gap={5}>
<MenuItem onClick={() => navigate("/upload")}>
<Icon name="upload" size={24} />
<FormattedMessage defaultMessage="Upload" />
</MenuItem>
<MenuItem onClick={() => navigate("/dashboard")}>
<Icon name="signal" size={24} />
<FormattedMessage defaultMessage="Dashboard" />
</MenuItem>
</Menu>
)}
<Menu
menuClassName="ctx-menu"
@ -83,19 +91,23 @@ export function HeaderNav() {
gap={5}>
<MenuItem onClick={() => navigate(profileLink(undefined, login.pubkey))}>
<Icon name="user" size={24} />
<FormattedMessage defaultMessage="Profile" id="itPgxd" />
<FormattedMessage defaultMessage="Profile" />
</MenuItem>
<MenuItem onClick={() => navigate("/settings")}>
<Icon name="settings" size={24} />
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
<FormattedMessage defaultMessage="Settings" />
</MenuItem>
<MenuItem onClick={() => navigate("/widgets")}>
<Icon name="widget" size={24} />
<FormattedMessage defaultMessage="Widgets" id="jgOqxt" />
<FormattedMessage defaultMessage="Widgets" />
</MenuItem>
<MenuItem onClick={() => window.open("https://discord.gg/Wtg6NVDdbT")}>
<Icon name="link" size={24} />
Discord
</MenuItem>
<MenuItem onClick={() => Login.logout()}>
<Icon name="logout" size={24} />
<FormattedMessage defaultMessage="Logout" id="C81/uG" />
<FormattedMessage defaultMessage="Logout" />
</MenuItem>
</Menu>
</div>
@ -123,32 +135,28 @@ export function HeaderNav() {
);
}
if (!layoutState.showHeader) return;
return (
<div className="flex justify-between items-center">
<div className="flex justify-between items-center gap-4">
<div className="flex gap-4 items-center m-2">
<NavLinkIcon
name="hamburger"
className="!opacity-100 max-xl:hidden"
onClick={() => {
layoutState.update(c => {
c.leftNav = !c.leftNav;
return { ...c };
});
}}
/>
{layoutState.leftNav && (
<NavLinkIcon
name="hamburger"
className="!opacity-100 max-xl:hidden"
onClick={() => {
layoutState.update(c => {
c.leftNavExpand = !c.leftNavExpand;
return { ...c };
});
}}
/>
)}
<Link to="/">
<Logo width={33} />
</Link>
</div>
<SearchBar />
<div className="flex items-center gap-3">
<Link
to="https://discord.gg/Wtg6NVDdbT"
target="_blank"
className="flex items-center max-md:hidden gap-1 bg-layer-1 hover:bg-layer-2 font-bold p-2 rounded-xl">
<Icon name="link" />
Discord
</Link>
{langSelector()}
{loggedIn()}
{loggedOut()}

View File

@ -1,29 +1,29 @@
import { useContext } from "react";
import { LayoutContext } from "./context";
import { useLayout } from "./context";
import { NavLinkIcon } from "./nav-icon";
import { FormattedMessage } from "react-intl";
export function LeftNav() {
const layout = useContext(LayoutContext);
const layout = useLayout();
if (layout.leftNav === false) return;
return (
<div className="flex flex-col gap-4 p-2 max-xl:hidden">
<NavLinkIcon name="signal" route="/streams" className="flex gap-2 items-center">
{layout.leftNav && (
{layout.leftNavExpand && (
<span className="pr-3">
<FormattedMessage defaultMessage="Streams" />
</span>
)}
</NavLinkIcon>
<NavLinkIcon name="play-circle" route="/videos" className="flex gap-2 items-center">
{layout.leftNav && (
{layout.leftNavExpand && (
<span className="pr-3">
<FormattedMessage defaultMessage="Videos" />
</span>
)}
</NavLinkIcon>
<NavLinkIcon name="grid" route="/category" className="flex gap-2 items-center">
{layout.leftNav && (
{layout.leftNavExpand && (
<span className="pr-3">
<FormattedMessage defaultMessage="Categories" />
</span>

View File

@ -10,13 +10,12 @@ export function SearchBar() {
const [search, setSearch] = useState(term ?? "");
return (
<div className="pr-4 h-fit flex items-center rounded-full px-3 py-1 lg:border lg:border-layer-2">
<div className="pr-4 h-fit flex items-center rounded-full px-3 py-1 border border-layer-2 max-xl:min-w-0">
<input
type="text"
className="reset max-lg:hidden bg-transparent"
className="reset bg-transparent"
placeholder={formatMessage({
defaultMessage: "Search",
id: "xmcVZ0",
})}
value={search}
onChange={e => setSearch(e.target.value)}

View File

@ -7,11 +7,14 @@ import { StreamPage } from "./stream-page";
import { VideoPage } from "./video";
import { EventEmbed as NostrEventElement } from "@/element/event-embed";
import { FormattedMessage } from "react-intl";
import { useLayout } from "./layout/context";
import classNames from "classnames";
export function LinkHandler() {
const location = useLocation();
const evPreload = getEventFromLocationState(location.state);
const link = useStreamLink();
const layoutContext = useLayout();
if (!link) return;
@ -23,7 +26,7 @@ export function LinkHandler() {
);
} else if (link.kind === EventKind.LiveEvent) {
return (
<div className="h-[calc(100dvh-52px)] w-full">
<div className={classNames(layoutContext.showHeader ? "h-[calc(100dvh-44px)]" : "h-[calc(100dvh)]", "w-full")}>
<StreamPage link={link} evPreload={evPreload} />
</div>
);

View File

@ -1,27 +0,0 @@
import { LIVE_STREAM } from "@/const";
import { LiveChat } from "@/element/live-chat";
import { SendZapsDialog } from "@/element/send-zap";
import { EventBuilder, NostrLink } from "@snort/system";
export default function MockPage() {
const pubkey = "cf45a6ba1363ad7ed213a078e710d24115ae721c9b47bd1ebf4458eaefb4c2a5";
const fakeStream = new EventBuilder()
.kind(LIVE_STREAM)
.pubKey(pubkey)
.tag(["d", "mock"])
.tag(["title", "Example Stream"])
.tag(["summary", "An example mock stream for debugging"])
.tag(["streaming", "https://example.com/live.m3u8"])
.tag(["t", "nostr"])
.tag(["t", "mock"])
.processContent()
.build();
const fakeStreamLink = NostrLink.fromEvent(fakeStream);
return (
<div className="">
<LiveChat link={fakeStreamLink} ev={fakeStream} height={600} />
<SendZapsDialog lnurl="donate@snort.social" aTag={fakeStreamLink.toEventTag()![1]} pubkey={pubkey} />
</div>
);
}

View File

@ -23,7 +23,7 @@ import { Goal } from "@/element/goal";
import { TopZappers } from "@/element/top-zappers";
import { useProfileClips } from "@/hooks/clips";
import VideoGrid from "@/element/video-grid";
import { ClipTile } from "@/element/clip-tile";
import { ClipTile } from "@/element/stream/clip-tile";
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";

View File

@ -15,7 +15,7 @@ export function RootPage() {
))}
</div>
</div>
<VideoGridSorted evs={streams} showEnded={false} showPopular={true} showRecentClips={true} />
<VideoGridSorted evs={streams} showEnded={false} showPopular={true} showRecentClips={false} />
</div>
);
}

View File

@ -1,17 +1,19 @@
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { Helmet } from "react-helmet";
import { Suspense, lazy } from "react";
import { Suspense, lazy, useEffect } from "react";
import { useMediaQuery } from "usehooks-ts";
const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
const LiveVideoPlayer = lazy(() => import("@/element/stream/live-video-player"));
import { extractStreamInfo, getHost } from "@/utils";
import { LiveChat } from "@/element/live-chat";
import { LiveChat } from "@/element/chat/live-chat";
import { useZapGoal } from "@/hooks/goals";
import { StreamCards } from "@/element/stream-cards";
import { ContentWarningOverlay, useContentWarning } from "@/element/nsfw";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { StreamState } from "@/const";
import { StreamInfo } from "@/element/stream-info";
import { StreamInfo } from "@/element/stream/stream-info";
import { useLayout } from "./layout/context";
import { StreamContextProvider } from "@/element/stream/stream-state";
export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) {
const ev = useCurrentStreamFeed(link, true, evPreload);
@ -31,6 +33,25 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
const goal = useZapGoal(goalTag);
const isDesktop = useMediaQuery("(min-width: 1280px)");
const isGrownUp = useContentWarning();
const layout = useLayout();
useEffect(() => {
if (layout.leftNav) {
layout.update(c => {
c.leftNav = false;
return { ...c };
});
}
}, [layout]);
useEffect(() => {
return () => {
layout.update(c => {
c.leftNav = true;
return { ...c };
});
};
}, []);
if (contentWarning && !isGrownUp) {
return <ContentWarningOverlay />;
@ -42,38 +63,42 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
...(tags ?? []),
].join(", ");
return (
<div className="xl:grid xl:grid-cols-[auto_450px] 2xl:xl:grid-cols-[auto_500px] max-xl:flex max-xl:flex-col xl:gap-4 max-xl:gap-1 h-full">
<Helmet>
<title>{`${title} - zap.stream`}</title>
<meta name="description" content={descriptionContent} />
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
<meta property="og:type" content="video" />
<meta property="og:title" content={title} />
<meta property="og:description" content={descriptionContent} />
<meta property="og:image" content={image ?? ""} />
</Helmet>
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
<Suspense>
<LiveVideoPlayer
title={title}
stream={status === StreamState.Live ? stream : recording}
poster={image}
status={status}
className="max-xl:max-h-[30vh] xl:w-full mx-auto"
/>
</Suspense>
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
{isDesktop && <StreamCards host={host} />}
<StreamContextProvider link={link}>
<div className="xl:grid xl:grid-cols-[auto_450px] 2xl:xl:grid-cols-[auto_500px] max-xl:flex max-xl:flex-col xl:gap-4 max-xl:gap-1 h-full">
<Helmet>
<title>{`${title} - zap.stream`}</title>
<meta name="description" content={descriptionContent} />
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
<meta property="og:type" content="video" />
<meta property="og:title" content={title} />
<meta property="og:description" content={descriptionContent} />
<meta property="og:image" content={image ?? ""} />
</Helmet>
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
<Suspense>
<LiveVideoPlayer
title={title}
stream={status === StreamState.Live ? stream : recording}
poster={image}
status={status}
className="max-xl:max-h-[30vh] xl:w-full mx-auto"
/>
</Suspense>
<div className="lg:px-5 max-lg:px-2">
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
{isDesktop && <StreamCards host={host} />}
</div>
</div>
<LiveChat
link={evLink ?? link}
ev={ev}
goal={goal}
canWrite={status === StreamState.Live}
adjustLayout={!isDesktop}
showGoal={true}
className="min-h-0 xl:border xl:border-layer-2 xl:rounded-xl xl:p-3 max-xl:px-2 h-inherit"
/>
</div>
<LiveChat
link={evLink ?? link}
ev={ev}
goal={goal}
canWrite={status === StreamState.Live}
showTopZappers={isDesktop}
showGoal={true}
className="min-h-0 xl:border xl:border-layer-2 xl:rounded-xl xl:p-3 max-xl:px-2 h-inherit"
/>
</div>
</StreamContextProvider>
);
}

View File

@ -1 +1,8 @@
export function UploadPage() {}
export function UploadPage() {
return (
<div>
<h1>Upload</h1>
<b>Coming Soon..</b>
</div>
);
}

View File

@ -1,28 +1,66 @@
import { StreamInfo } from "@/element/stream-info";
import { Textarea } from "@/element/chat/textarea";
import { WriteMessage } from "@/element/chat/write-message";
import { FollowButton } from "@/element/follow-button";
import { Profile, getName } from "@/element/profile";
import { SendZapsDialog } from "@/element/send-zap";
import { ShareMenu } from "@/element/share-menu";
import { StreamSummary } from "@/element/stream/summary";
import VideoComments from "@/element/video/comments";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { getHost, extractStreamInfo } from "@/utils";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
const { formatMessage } = useIntl();
const ev = useCurrentStreamFeed(link, true, evPreload);
const [newComment, setNewComment] = useState("");
const host = getHost(ev);
const evLink = ev ? NostrLink.fromEvent(ev) : undefined;
const {
title,
summary,
image,
status,
tags,
contentWarning,
stream,
recording,
goal: goalTag,
} = extractStreamInfo(ev);
const { title, summary, image, contentWarning, recording } = extractStreamInfo(ev);
const profile = useUserProfile(host);
const zapTarget = profile?.lud16 ?? profile?.lud06;
return (
<div className="p-4 w-[80dvw] mx-auto">
<video src={recording} controls className="w-full aspect-video" />
<StreamInfo ev={ev as TaggedNostrEvent} />
<video src={recording} controls className="w-full aspect-video" poster={image} />
<div className="grid grid-cols-[auto_450px]">
<div className="flex flex-col gap-4">
<div className="font-medium text-xl">{title}</div>
<div className="flex justify-between">
{/* PROFILE SECTION */}
<div className="flex gap-2 items-center">
<Profile pubkey={host} />
<FollowButton pubkey={host} />
</div>
{/* ACTIONS */}
<div className="flex gap-2">
{ev && (
<>
<ShareMenu ev={ev} />
{zapTarget && (
<SendZapsDialog
lnurl={zapTarget}
pubkey={host}
aTag={link.tagKey}
targetName={getName(ev.pubkey, profile)}
/>
)}
</>
)}
</div>
</div>
{summary && <StreamSummary text={summary} />}
<h3>
<FormattedMessage defaultMessage="Comments" />
</h3>
<div>
<WriteMessage link={link} emojiPacks={[]} kind={1} />
</div>
<VideoComments link={link} />
</div>
</div>
</div>
);
}