feat: channel

This commit is contained in:
Artemy 2023-08-09 21:38:13 +03:00
parent ce520717e0
commit 0422fb4875
9 changed files with 166 additions and 14 deletions

View file

@ -5,7 +5,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>ULTYT</title>
</head> </head>
<body> <body>

8
package-lock.json generated
View file

@ -11,7 +11,7 @@
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@nextui-org/react": "^2.0.7", "@nextui-org/react": "^2.0.7",
"framer-motion": "^10.15.1", "framer-motion": "^10.15.1",
"piped-api": "^1.1.6", "piped-api": "^1.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.14.2" "react-router-dom": "^6.14.2"
@ -4439,9 +4439,9 @@
} }
}, },
"node_modules/piped-api": { "node_modules/piped-api": {
"version": "1.1.6", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/piped-api/-/piped-api-1.1.6.tgz", "resolved": "https://registry.npmjs.org/piped-api/-/piped-api-1.2.0.tgz",
"integrity": "sha512-Tyn6/x/nBass469Io5zyILKylQI6sHE0MIYPYgD5nD232TmBF3G0OU+AQrvqU6cCla+Bkff3oFo+4GhnJKeiuA==", "integrity": "sha512-180w5tI7/I4bPQ0+Jwl5fj84cd+bffq4XihK9lt6zbUQdmbIcxVsJ/zi83rUAJGAijCp0/BuZeGywpKp9Lt5NA==",
"dependencies": { "dependencies": {
"axios": "^1.4.0" "axios": "^1.4.0"
} }

View file

@ -13,7 +13,7 @@
"@heroicons/react": "^2.0.18", "@heroicons/react": "^2.0.18",
"@nextui-org/react": "^2.0.7", "@nextui-org/react": "^2.0.7",
"framer-motion": "^10.15.1", "framer-motion": "^10.15.1",
"piped-api": "^1.1.6", "piped-api": "^1.2.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.14.2" "react-router-dom": "^6.14.2"

View file

@ -3,8 +3,9 @@ import "./App.css";
import { NextUIProvider } from "@nextui-org/react"; import { NextUIProvider } from "@nextui-org/react";
import { PipedAPI } from "piped-api"; import { PipedAPI } from "piped-api";
import { HashRouter, Navigate, Route, Routes } from "react-router-dom"; import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import Trending from "./pages/trending"; import TrendingPage from "./pages/trending";
import { NavbarComponent } from "./components/navbar"; import { NavbarComponent } from "./components/navbar";
import ChannelPage from "./pages/channel";
declare global { declare global {
interface Window { interface Window {
@ -12,7 +13,7 @@ declare global {
} }
} }
function App() { function App() {
window.piped_api = new PipedAPI(); //"https://ytapi.dc09.ru"); window.piped_api = new PipedAPI("https://ytapi.dc09.ru");
return ( return (
<NextUIProvider> <NextUIProvider>
@ -20,7 +21,8 @@ function App() {
<NavbarComponent /> <NavbarComponent />
<Routes> <Routes>
<Route path="/" element={<Navigate to="/trending" />} /> <Route path="/" element={<Navigate to="/trending" />} />
<Route path="/trending" element={<Trending />} /> <Route path="/trending" element={<TrendingPage />} />
<Route path="/channel/:id" element={<ChannelPage />} />
</Routes> </Routes>
</HashRouter> </HashRouter>
</NextUIProvider> </NextUIProvider>

View file

@ -6,3 +6,7 @@ export function shortenNumber(number: number) {
} }
return number.toString(); return number.toString();
} }
export function capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View file

@ -10,7 +10,7 @@ import { Video } from "piped-api/dist/types";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { CheckCircleIcon, EyeIcon } from "@heroicons/react/24/solid"; import { CheckCircleIcon, EyeIcon } from "@heroicons/react/24/solid";
import { shortenNumber } from "./utils"; import { shortenNumber } from "./utils";
export function VideoComponent({ video }: VideoComponentProps) { export function VideoComponent({ video, uploaderAvatar }: VideoComponentProps) {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
@ -24,7 +24,10 @@ export function VideoComponent({ video }: VideoComponentProps) {
<img className="w-full rounded-xl" src={video.thumbnail} /> <img className="w-full rounded-xl" src={video.thumbnail} />
</CardHeader> </CardHeader>
<CardBody className="flex gap-3 flex-row"> <CardBody className="flex gap-3 flex-row">
<Avatar className="flex-none" src={video.uploaderAvatar} /> <Avatar
className="flex-none"
src={uploaderAvatar ? uploaderAvatar : video.uploaderAvatar}
/>
<div className="flex gap-3 flex-col"> <div className="flex gap-3 flex-col">
<Link color="foreground" href={`#${video.url}`}> <Link color="foreground" href={`#${video.url}`}>
{video.title} {video.title}
@ -61,7 +64,19 @@ export function SkeletonVideoComponent() {
</Card> </Card>
); );
} }
export function VideoContainer({ children }: VideoContainerProps) {
return (
<div className="grid md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 gap-2 p-4">
{children}
</div>
);
}
type VideoComponentProps = { type VideoComponentProps = {
video: Video; video: Video;
uploaderAvatar?: string;
};
type VideoContainerProps = {
children: React.ReactNode;
}; };

129
src/pages/channel.tsx Normal file
View file

@ -0,0 +1,129 @@
import { Channel, Tab, Video } from "piped-api/dist/types";
import { useEffect, useState } from "react";
import {
SkeletonVideoComponent,
VideoComponent,
VideoContainer,
} from "../components/video";
import { useParams, useSearchParams } from "react-router-dom";
import { Card, CardFooter } from "@nextui-org/react";
import { capitalize } from "../components/utils";
export default function ChannelPage() {
const [channel, setChannel] = useState<Channel>();
const [tab, setTab] = useState<Tab>();
const { id } = useParams();
const [searchParams, setSearchParams] = useSearchParams();
const tabId = searchParams.get("tabId") || "videos";
useEffect(() => {
async function getChannel() {
const channel = (await window.piped_api.channel(id || "")) as Channel;
setChannel(channel);
}
async function getTab(name: string) {
const tab = await window.piped_api.channelTabs(channel?.tabs[name].data);
setTab(tab);
}
if (!channel) {
getChannel();
}
if (!tab && tabId !== "videos") {
getTab(tabId);
}
});
if (!channel) {
return (
<div className="grid md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 gap-2 p-4">
{[...Array(20).keys()].map((num) => (
<SkeletonVideoComponent key={num} />
))}
</div>
);
} else {
return (
<div className="grid grid-rows p-4">
<div className="">
<Card radius="lg" shadow="none">
<div className="p-4">
<img className="w-full rounded-xl" src={channel.bannerUrl} />
</div>
<CardFooter className="p-4 justify-between ">
<div className="flex flex-col">
<div className="flex flex-row gap-2 text-xl font-bold pl-2">
{channel.name}
</div>
</div>
</CardFooter>
</Card>
</div>
<div className="flex flex-row flex-wrap gap-2 bg-white rounded-xl mt-4">
<div
style={{ cursor: "pointer" }}
onClick={() => {
setSearchParams({ tabId: "videos" });
}}
key="videos"
className="text-xl font-bold p-4"
>
Videos
</div>
{channel.tabs.map((tab, index) => {
return (
<div
style={{ cursor: "pointer" }}
onClick={() => {
setSearchParams({ tabId: String(index) });
setTab(undefined);
}}
key={tab.name}
className="text-xl font-bold p-4"
>
{capitalize(tab.name)}
</div>
);
})}
</div>
{tabId !== "videos" ? (
<VideoContainer>
{tab?.content.map((video) => {
if (video.type === "stream") {
video = video as Video;
return (
<VideoComponent
video={video}
key={video.url}
uploaderAvatar={channel.avatarUrl}
/>
);
} else {
return (
<div className="" key={Math.random()}>
Soon...
</div>
);
}
})}
</VideoContainer>
) : (
<VideoContainer>
{channel.relatedStreams.map((video) => (
<VideoComponent
video={video}
key={video.url}
uploaderAvatar={channel.avatarUrl}
/>
))}
</VideoContainer>
)}
</div>
);
}
}

View file

@ -2,12 +2,12 @@ import { Video } from "piped-api/dist/types";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SkeletonVideoComponent, VideoComponent } from "../components/video"; import { SkeletonVideoComponent, VideoComponent } from "../components/video";
export default function Trending() { export default function TrendingPage() {
const [trending, setTrending] = useState([] as Video[]); const [trending, setTrending] = useState([] as Video[]);
useEffect(() => { useEffect(() => {
async function getTrending() { async function getTrending() {
const trending = await window.piped_api.trending("US"); //await window.piped_api.search("artegoser"); const trending = await window.piped_api.trending("US");
setTrending(trending); setTrending(trending);
} }

View file

@ -18,7 +18,9 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"noImplicitAny": false
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]