mirror of
https://github.com/fergalmoran/xtreamium.git
synced 2026-01-06 17:06:38 +00:00
Onboarding screen (not working) added
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"axios": "^0.26.1",
|
||||
"classnames": "^2.3.1",
|
||||
"hls.js": "^1.1.5",
|
||||
"postcss": "^8.4.12",
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from "react";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import { Layout } from "./containers";
|
||||
import { OnboardingPage } from "./pages";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
|
||||
<Routes>
|
||||
<Route path="/*" element={<Layout />} />
|
||||
{localStorage.getItem("server") ? (
|
||||
<Route path="/*" element={<OnboardingPage />} />
|
||||
) : (
|
||||
<Route path="/*" element={<Layout />} />
|
||||
)}
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
BIN
frontend/src/assets/images/love-tv.jpg
Normal file
BIN
frontend/src/assets/images/love-tv.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
|
||||
interface IEPGComponentProps {
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const Header = () => {
|
||||
<BsSearch className="w-4 h-4" aria-hidden="true" />
|
||||
</div>
|
||||
<Input
|
||||
className="h-8 pl-8 text-gray-700"
|
||||
className="pl-8 text-gray-700"
|
||||
placeholder="Search for channels"
|
||||
aria-label="Search"
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React from "react";
|
||||
import { NavLink, Route } from "react-router-dom";
|
||||
import { Channel } from "../../models/channel";
|
||||
import { ApiService } from "../../services";
|
||||
|
||||
const SidebarContent = () => {
|
||||
const [channels, setChannels] = React.useState<Channel[]>([]);
|
||||
const [filteredChannels, setFilteredChannels] = React.useState<Channel[]>([]);
|
||||
React.useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const res = await fetch(`${process.env.REACT_APP_API_URL}/channels`);
|
||||
const data = await res.json();
|
||||
setChannels(data);
|
||||
setFilteredChannels(data);
|
||||
const res = await ApiService.getChannels();
|
||||
if (res) {
|
||||
setChannels(res);
|
||||
setFilteredChannels(res);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChannels().catch(console.error);
|
||||
|
||||
@@ -3,5 +3,6 @@ import Badge from "./badge.component";
|
||||
import Button from "./button.component";
|
||||
import { Dropdown, DropdownItem } from "./dropdown.component";
|
||||
import Input from "./input.component";
|
||||
import Label from "./label.component";
|
||||
|
||||
export { Avatar, Badge, Button, Input, Dropdown, DropdownItem };
|
||||
export { Avatar, Badge, Button, Input, Dropdown, DropdownItem, Label };
|
||||
|
||||
36
frontend/src/components/widgets/label.component.tsx
Normal file
36
frontend/src/components/widgets/label.component.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import { defaultTheme } from "../../constants";
|
||||
|
||||
export interface LabelProps extends React.HTMLAttributes<HTMLLabelElement> {
|
||||
check?: boolean;
|
||||
radio?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Label = React.forwardRef<HTMLLabelElement, LabelProps>(function Label(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const { children, check, radio, disabled, className, ...other } = props;
|
||||
const { label } = defaultTheme;
|
||||
|
||||
const baseStyle = label.base;
|
||||
const checkStyle = label.check;
|
||||
const disabledStyle = label.disabled;
|
||||
|
||||
const cls = classNames(
|
||||
baseStyle,
|
||||
// check and radio are interchangeable
|
||||
(check || radio) && checkStyle,
|
||||
disabled && disabledStyle,
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<label className={cls} ref={ref} {...other}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
});
|
||||
export default Label;
|
||||
@@ -130,7 +130,7 @@ const defaultTheme = {
|
||||
},
|
||||
// Input
|
||||
input: {
|
||||
base: "block w-full text-sm focus:outline-none dark:text-gray-300 leading-5 rounded-md",
|
||||
base: "block w-full text-sm focus:outline-none dark:text-gray-300 leading-5 rounded-md h-8 p-2",
|
||||
active:
|
||||
"focus:border-purple-400 border-gray-300 dark:border-gray-600 focus:ring focus:ring-purple-300 dark:focus:border-gray-600 dark:focus:ring-gray-300 dark:bg-gray-700",
|
||||
disabled: "cursor-not-allowed opacity-50 bg-gray-300 dark:bg-gray-800",
|
||||
|
||||
@@ -11,8 +11,16 @@ import {
|
||||
useMedia,
|
||||
} from "../utils/chromecast";
|
||||
import { toast } from "react-toastify";
|
||||
import { Table, TableBody, TableCell, TableContainer, TableHeader, TableRow } from "../components/widgets/table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../components/widgets/table";
|
||||
import { Avatar, Badge, Button } from "../components/widgets";
|
||||
import { ApiService } from "../services";
|
||||
|
||||
const ChannelPage = () => {
|
||||
let params = useParams();
|
||||
@@ -24,31 +32,20 @@ const ChannelPage = () => {
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const res = await fetch(
|
||||
`${process.env.REACT_APP_API_URL}/streams/${params.channelId}`
|
||||
);
|
||||
const data = await res.json();
|
||||
setStreams(data);
|
||||
if (params.channelId) {
|
||||
const data = await ApiService.getStreams(params.channelId);
|
||||
setStreams(data);
|
||||
}
|
||||
};
|
||||
|
||||
fetchChannels().catch(console.error);
|
||||
}, [params.channelId]);
|
||||
|
||||
const _getStreamUrl = async (streamId: number) => {
|
||||
const res = await fetch(
|
||||
`${process.env.REACT_APP_API_URL}/live/stream/url/${streamId}`
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
alert("Failed to get stream url");
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
return data?.url;
|
||||
};
|
||||
|
||||
|
||||
const _cast = React.useCallback(
|
||||
async (streamId: number) => {
|
||||
const streamUrl = await _getStreamUrl(streamId);
|
||||
const streamUrl = await ApiService.getStreamUrl(streamId);
|
||||
if (streamUrl) {
|
||||
await media.playMedia(streamUrl);
|
||||
}
|
||||
@@ -60,7 +57,7 @@ const ChannelPage = () => {
|
||||
console.log("channel.page", "handleXHR", args);
|
||||
};
|
||||
const playStream = async (streamId: number) => {
|
||||
const url = await _getStreamUrl(streamId);
|
||||
const url = await ApiService.getStreamUrl(streamId);
|
||||
if (url) {
|
||||
const mpv_args =
|
||||
"--keep-open=yes\n--geometry=1024x768-0-0\n--ontop\n--screen=2\n--ytdl-format=bestvideo[ext=mp4][height<=?720]+bestaudio[ext=m4a]\n--border=no".split(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ChannelPage from "./channel.page";
|
||||
import HomePage from "./home.page";
|
||||
import OnboardingPage from "./onboarding.page";
|
||||
import PlayerPage from "./player.page";
|
||||
|
||||
export { HomePage, ChannelPage, PlayerPage };
|
||||
export { HomePage, ChannelPage, PlayerPage, OnboardingPage };
|
||||
|
||||
60
frontend/src/pages/onboarding.page.tsx
Normal file
60
frontend/src/pages/onboarding.page.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import ImageLight from "../assets/images/love-tv.jpg";
|
||||
import { Button, Input, Label } from "../components/widgets";
|
||||
const OnboardingPage = () => {
|
||||
const _setupOnboarding = () => {};
|
||||
return (
|
||||
<div className="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900">
|
||||
<div className="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
||||
<div className="flex flex-col overflow-y-auto md:flex-row">
|
||||
<div className="h-32 md:h-auto md:w-1/2">
|
||||
<img
|
||||
aria-hidden="true"
|
||||
className="object-cover w-full h-full dark:hidden"
|
||||
src={ImageLight}
|
||||
alt="Office"
|
||||
/>
|
||||
<img
|
||||
aria-hidden="true"
|
||||
className="hidden object-cover w-full h-full dark:block"
|
||||
src={ImageLight}
|
||||
alt="Office"
|
||||
/>
|
||||
</div>
|
||||
<main className="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
|
||||
<div className="w-full">
|
||||
<h1 className="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Login
|
||||
</h1>
|
||||
<Label>
|
||||
<span>Email</span>
|
||||
<Input
|
||||
className="mt-1"
|
||||
type="email"
|
||||
placeholder="john@doe.com"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Label className="mt-4">
|
||||
<span>Password</span>
|
||||
<Input
|
||||
className="mt-1"
|
||||
type="password"
|
||||
placeholder="***************"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Button className="mt-4" block onClick={_setupOnboarding}>
|
||||
Log in
|
||||
</Button>
|
||||
|
||||
<hr className="my-8" />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingPage;
|
||||
28
frontend/src/services/api.service.ts
Normal file
28
frontend/src/services/api.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import http from "./http.service";
|
||||
import { Channel } from "../models/channel";
|
||||
import { Stream } from "../models/stream";
|
||||
|
||||
class ApiService {
|
||||
public getChannels = async (): Promise<Channel[]> => {
|
||||
const response = await http.get("/channels");
|
||||
return response.data;
|
||||
};
|
||||
|
||||
public getStreams = async (channelId: string): Promise<Stream[]> => {
|
||||
const response = await http.get(`/streams/${channelId}`);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
public getStreamUrl = async (
|
||||
streamId: number
|
||||
): Promise<string | undefined> => {
|
||||
const res = await http.get(`/live/stream/url/${streamId}`);
|
||||
if (res.status !== 200) {
|
||||
alert("Failed to get stream url");
|
||||
return;
|
||||
}
|
||||
return res?.data.url;
|
||||
};
|
||||
}
|
||||
|
||||
export default new ApiService();
|
||||
16
frontend/src/services/http.service.ts
Normal file
16
frontend/src/services/http.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import axios from "axios";
|
||||
export default axios.create({
|
||||
baseURL: process.env.REACT_APP_API_URL,
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"x-xtream-server": JSON.parse(localStorage.getItem("server") || "{}")[
|
||||
"server"
|
||||
],
|
||||
"x-xtream-username": JSON.parse(localStorage.getItem("server") || "{}")[
|
||||
"username"
|
||||
],
|
||||
"x-xtream-password": JSON.parse(localStorage.getItem("server") || "{}")[
|
||||
"password"
|
||||
],
|
||||
},
|
||||
});
|
||||
3
frontend/src/services/index.ts
Normal file
3
frontend/src/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import ApiService from "./api.service";
|
||||
export { ApiService };
|
||||
|
||||
@@ -2495,6 +2495,13 @@ axe-core@^4.3.5:
|
||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
|
||||
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
|
||||
|
||||
axios@^0.26.1:
|
||||
version "0.26.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9"
|
||||
integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.8"
|
||||
|
||||
axobject-query@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||
@@ -4300,7 +4307,7 @@ focus-lock@^0.10.2:
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
follow-redirects@^1.0.0:
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.8:
|
||||
version "1.14.9"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
|
||||
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
|
||||
|
||||
Reference in New Issue
Block a user