mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-26 09:31:50 +01:00
admin page + beginning of settings page
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
e267482d33
commit
de68438793
12
src/components/utils/Divider.tsx
Normal file
12
src/components/utils/Divider.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
export function Divider(props: { marginClass?: string }) {
|
||||||
|
return (
|
||||||
|
<hr
|
||||||
|
className={classNames(
|
||||||
|
"w-full h-px border-0 bg-utils-divider bg-opacity-50",
|
||||||
|
props.marginClass ?? "my-8"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
79
src/pages/Settings.tsx
Normal file
79
src/pages/Settings.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
|
import { Divider } from "@/components/utils/Divider";
|
||||||
|
import { Heading1 } from "@/components/utils/Text";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
import { SubPageLayout } from "./layouts/SubPageLayout";
|
||||||
|
|
||||||
|
// TODO Put all of this not here (when I'm done writing them)
|
||||||
|
|
||||||
|
function SidebarSection(props: { title: string; children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<p className="text-sm font-bold uppercase text-settings-sidebar-type-secondary mb-2">
|
||||||
|
{props.title}
|
||||||
|
</p>
|
||||||
|
{props.children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SidebarLink(props: { children: React.ReactNode; icon: Icons }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full px-2 py-1 flex items-center space-x-3">
|
||||||
|
<Icon
|
||||||
|
className="text-2xl text-settings-sidebar-type-icon"
|
||||||
|
icon={props.icon}
|
||||||
|
/>
|
||||||
|
<span>{props.children}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsSidebar() {
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
const hostname = location.hostname;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="sticky top-24 text-settings-sidebar-type-inactive">
|
||||||
|
<SidebarSection title="Settings">
|
||||||
|
<SidebarLink icon={Icons.WAND}>Account</SidebarLink>
|
||||||
|
</SidebarSection>
|
||||||
|
<Divider />
|
||||||
|
<SidebarSection title="App information">
|
||||||
|
<div className="flex justify-between items-center space-x-3">
|
||||||
|
<span>Version</span>
|
||||||
|
<span>{conf().APP_VERSION}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center space-x-3">
|
||||||
|
<span>Domain</span>
|
||||||
|
<span className="text-right">{hostname}</span>
|
||||||
|
</div>
|
||||||
|
</SidebarSection>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsLayout(props: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<WideContainer ultraWide>
|
||||||
|
<div className="grid grid-cols-[260px,1fr] gap-12">
|
||||||
|
<SettingsSidebar />
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</WideContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsPage() {
|
||||||
|
return (
|
||||||
|
<SubPageLayout>
|
||||||
|
<SettingsLayout>
|
||||||
|
<Heading1>Setting</Heading1>
|
||||||
|
</SettingsLayout>
|
||||||
|
</SubPageLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { ThinContainer } from "@/components/layout/ThinContainer";
|
import { ThinContainer } from "@/components/layout/ThinContainer";
|
||||||
import { Heading1, Paragraph } from "@/components/utils/Text";
|
import { Heading1, Paragraph } from "@/components/utils/Text";
|
||||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||||
|
import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart";
|
||||||
|
import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart";
|
||||||
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
|
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
|
||||||
|
|
||||||
export function AdminPage() {
|
export function AdminPage() {
|
||||||
@ -10,7 +12,9 @@ export function AdminPage() {
|
|||||||
<Heading1>Admin tools</Heading1>
|
<Heading1>Admin tools</Heading1>
|
||||||
<Paragraph>Useful tools to test out your current deployment</Paragraph>
|
<Paragraph>Useful tools to test out your current deployment</Paragraph>
|
||||||
|
|
||||||
|
<ConfigValuesPart />
|
||||||
<WorkerTestPart />
|
<WorkerTestPart />
|
||||||
|
<TMDBTestPart />
|
||||||
</ThinContainer>
|
</ThinContainer>
|
||||||
</SubPageLayout>
|
</SubPageLayout>
|
||||||
);
|
);
|
||||||
|
32
src/pages/parts/admin/ConfigValuesPart.tsx
Normal file
32
src/pages/parts/admin/ConfigValuesPart.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { Divider } from "@/components/utils/Divider";
|
||||||
|
import { Heading2 } from "@/components/utils/Text";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
function ConfigValue(props: { name: string; children?: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex">
|
||||||
|
<p className="flex-1 font-bold text-white">{props.name}</p>
|
||||||
|
<p>{props.children}</p>
|
||||||
|
</div>
|
||||||
|
<Divider marginClass="my-3" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfigValuesPart() {
|
||||||
|
const normalRouter = conf().NORMAL_ROUTER;
|
||||||
|
const appVersion = conf().APP_VERSION;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading2 className="mb-8 mt-12">Configured values</Heading2>
|
||||||
|
<ConfigValue name="Routing mode">
|
||||||
|
{normalRouter ? "Normal routing" : "Hash based routing"}
|
||||||
|
</ConfigValue>
|
||||||
|
<ConfigValue name="Application version">v{appVersion}</ConfigValue>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
93
src/pages/parts/admin/TMDBTestPart.tsx
Normal file
93
src/pages/parts/admin/TMDBTestPart.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
|
import { getMediaDetails } from "@/backend/metadata/tmdb";
|
||||||
|
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
|
||||||
|
import { Button } from "@/components/Button";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { Box } from "@/components/layout/Box";
|
||||||
|
import { Spinner } from "@/components/layout/Spinner";
|
||||||
|
import { Heading2 } from "@/components/utils/Text";
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
export function TMDBTestPart() {
|
||||||
|
const tmdbApiKey = conf().TMDB_READ_API_KEY;
|
||||||
|
const [status, setStatus] = useState({
|
||||||
|
hasTested: false,
|
||||||
|
success: false,
|
||||||
|
errorText: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [testState, runTests] = useAsyncFn(async () => {
|
||||||
|
setStatus({
|
||||||
|
hasTested: false,
|
||||||
|
success: false,
|
||||||
|
errorText: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tmdbApiKey.length === 0) {
|
||||||
|
return setStatus({
|
||||||
|
hasTested: true,
|
||||||
|
success: false,
|
||||||
|
errorText: "TMDB api key is not set",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const isJWT = tmdbApiKey.split(".").length > 2;
|
||||||
|
if (!isJWT) {
|
||||||
|
return setStatus({
|
||||||
|
hasTested: true,
|
||||||
|
success: false,
|
||||||
|
errorText: "TMDB api key is not a read only key",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await getMediaDetails("556574", TMDBContentTypes.MOVIE);
|
||||||
|
} catch (err) {
|
||||||
|
return setStatus({
|
||||||
|
hasTested: true,
|
||||||
|
success: false,
|
||||||
|
errorText:
|
||||||
|
"Failed to call tmdb, double check api key and your internet connection",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return setStatus({
|
||||||
|
hasTested: true,
|
||||||
|
success: true,
|
||||||
|
errorText: "",
|
||||||
|
});
|
||||||
|
}, [tmdbApiKey, setStatus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Heading2 className="mb-8 mt-12">TMDB tests</Heading2>
|
||||||
|
<Box>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
{!status.hasTested ? (
|
||||||
|
<p>Run the test to validate TMDB</p>
|
||||||
|
) : status.success ? (
|
||||||
|
<p className="flex items-center">
|
||||||
|
<Icon
|
||||||
|
icon={Icons.CIRCLE_CHECK}
|
||||||
|
className="text-video-scraping-success mr-2"
|
||||||
|
/>
|
||||||
|
TMDB is working as expected
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-white font-bold">TMDB is not working</p>
|
||||||
|
<p>{status.errorText}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button theme="purple" onClick={runTests}>
|
||||||
|
{testState.loading ? <Spinner className="text-base mr-2" /> : null}
|
||||||
|
Test TMDB
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { f } from "ofetch/dist/shared/ofetch.441891d5";
|
import { useMemo, useState } from "react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
import { useAsyncFn } from "react-use";
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
import { mwFetch } from "@/backend/helpers/fetch";
|
import { mwFetch } from "@/backend/helpers/fetch";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Box } from "@/components/layout/Box";
|
import { Box } from "@/components/layout/Box";
|
||||||
import { Divider } from "@/components/player/internals/ContextMenu/Misc";
|
import { Spinner } from "@/components/layout/Spinner";
|
||||||
|
import { Divider } from "@/components/utils/Divider";
|
||||||
import { Heading2 } from "@/components/utils/Text";
|
import { Heading2 } from "@/components/utils/Text";
|
||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export function WorkerTestPart() {
|
|||||||
{ id: string; status: "error" | "success"; error?: Error }[]
|
{ id: string; status: "error" | "success"; error?: Error }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const runTests = useAsyncFn(async () => {
|
const [testState, runTests] = useAsyncFn(async () => {
|
||||||
function updateWorker(id: string, data: (typeof workerState)[number]) {
|
function updateWorker(id: string, data: (typeof workerState)[number]) {
|
||||||
setWorkerState((s) => {
|
setWorkerState((s) => {
|
||||||
return [...s.filter((v) => v.id !== id), data];
|
return [...s.filter((v) => v.id !== id), data];
|
||||||
@ -62,6 +62,14 @@ export function WorkerTestPart() {
|
|||||||
setWorkerState([]);
|
setWorkerState([]);
|
||||||
for (const worker of workerList) {
|
for (const worker of workerList) {
|
||||||
try {
|
try {
|
||||||
|
if (worker.url.endsWith("/")) {
|
||||||
|
updateWorker(worker.id, {
|
||||||
|
id: worker.id,
|
||||||
|
status: "error",
|
||||||
|
error: new Error("URL ends with slash"),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
await mwFetch(worker.url, {
|
await mwFetch(worker.url, {
|
||||||
query: {
|
query: {
|
||||||
destination: "https://postman-echo.com/get",
|
destination: "https://postman-echo.com/get",
|
||||||
@ -83,8 +91,8 @@ export function WorkerTestPart() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Heading2 className="mb-0 mt-12">Worker tests</Heading2>
|
<Heading2 className="!mb-0 mt-12">Worker tests</Heading2>
|
||||||
<p className="mb-8 mt-2">15 workers registered</p>
|
<p className="mb-8 mt-2">{workerList.length} worker(s) registered</p>
|
||||||
<Box>
|
<Box>
|
||||||
{workerList.map((v, i) => {
|
{workerList.map((v, i) => {
|
||||||
const s = workerState.find((segment) => segment.id);
|
const s = workerState.find((segment) => segment.id);
|
||||||
@ -105,7 +113,10 @@ export function WorkerTestPart() {
|
|||||||
})}
|
})}
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button theme="purple">Run tests</Button>
|
<Button theme="purple" onClick={runTests}>
|
||||||
|
{testState.loading ? <Spinner className="text-base mr-2" /> : null}
|
||||||
|
Test workers
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
@ -17,6 +17,7 @@ import { DmcaPage } from "@/pages/Dmca";
|
|||||||
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
import { NotFoundPage } from "@/pages/errors/NotFoundPage";
|
||||||
import { HomePage } from "@/pages/HomePage";
|
import { HomePage } from "@/pages/HomePage";
|
||||||
import { PlayerView } from "@/pages/PlayerView";
|
import { PlayerView } from "@/pages/PlayerView";
|
||||||
|
import { SettingsPage } from "@/pages/Settings";
|
||||||
import { Layout } from "@/setup/Layout";
|
import { Layout } from "@/setup/Layout";
|
||||||
import { BookmarkContextProvider } from "@/state/bookmark";
|
import { BookmarkContextProvider } from "@/state/bookmark";
|
||||||
import { SettingsProvider } from "@/state/settings";
|
import { SettingsProvider } from "@/state/settings";
|
||||||
@ -103,6 +104,9 @@ function App() {
|
|||||||
<Route exact path="/faq" component={AboutPage} />
|
<Route exact path="/faq" component={AboutPage} />
|
||||||
<Route exact path="/dmca" component={DmcaPage} />
|
<Route exact path="/dmca" component={DmcaPage} />
|
||||||
|
|
||||||
|
{/* Settings page */}
|
||||||
|
<Route exact path="/settings" component={SettingsPage} />
|
||||||
|
|
||||||
{/* admin routes */}
|
{/* admin routes */}
|
||||||
<Route exact path="/admin" component={AdminPage} />
|
<Route exact path="/admin" component={AdminPage} />
|
||||||
|
|
||||||
|
@ -109,6 +109,21 @@ module.exports = {
|
|||||||
badgeText: "#5F5F7A"
|
badgeText: "#5F5F7A"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
sidebar: {
|
||||||
|
type: {
|
||||||
|
secondary: "#4B395F",
|
||||||
|
inactive: "#8D68A9",
|
||||||
|
icon: "#926CAD",
|
||||||
|
activated: "#CBA1E8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
utils: {
|
||||||
|
divider: "#353549"
|
||||||
|
},
|
||||||
|
|
||||||
// Error page
|
// Error page
|
||||||
errors: {
|
errors: {
|
||||||
card: "#12121B",
|
card: "#12121B",
|
||||||
|
Loading…
Reference in New Issue
Block a user