mirror of
https://github.com/artegoser/ultyt.git
synced 2024-11-23 12:26:25 +03:00
feat: channel
This commit is contained in:
parent
ce520717e0
commit
0422fb4875
9 changed files with 166 additions and 14 deletions
|
@ -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
8
package-lock.json
generated
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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
129
src/pages/channel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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" }]
|
||||||
|
|
Loading…
Add table
Reference in a new issue