mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-09-10 20:08:19 +02:00
Fix MangaCard/List
Style AddNotificationConnector
This commit is contained in:
@@ -4,12 +4,12 @@ import Settings from "./Components/Settings/Settings.tsx";
|
|||||||
import Header from "./Header.tsx";
|
import Header from "./Header.tsx";
|
||||||
import {createContext, useEffect, useState} from "react";
|
import {createContext, useEffect, useState} from "react";
|
||||||
import {V2} from "./apiClient/V2.ts";
|
import {V2} from "./apiClient/V2.ts";
|
||||||
import {GetManga, MangaContext } from './apiClient/MangaContext.tsx';
|
|
||||||
import { ApiContext } from './apiClient/ApiContext.tsx';
|
import { ApiContext } from './apiClient/ApiContext.tsx';
|
||||||
import MangaList from "./Components/Mangas/MangaList.tsx";
|
import MangaList from "./Components/Mangas/MangaList.tsx";
|
||||||
import {MangaConnector} from "./apiClient/data-contracts.ts";
|
import {Manga, MangaConnector} from "./apiClient/data-contracts.ts";
|
||||||
|
|
||||||
export const MangaConnectorContext = createContext<MangaConnector[]>([]);
|
export const MangaConnectorContext = createContext<MangaConnector[]>([]);
|
||||||
|
export const MangaContext = createContext<Manga[]>([]);
|
||||||
|
|
||||||
export default function App () {
|
export default function App () {
|
||||||
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
|
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api";
|
||||||
@@ -17,10 +17,24 @@ export default function App () {
|
|||||||
const [Api, setApi] = useState<V2>(new V2());
|
const [Api, setApi] = useState<V2>(new V2());
|
||||||
|
|
||||||
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
|
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
|
||||||
|
const [manga, setManga] = useState<Manga[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Api.mangaConnectorList().then(response => {
|
Api.mangaConnectorList().then(response => {
|
||||||
if (response.ok)
|
if (response.ok)
|
||||||
setMangaConnectors(response.data);
|
setMangaConnectors(response.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
Api.mangaList().then(response => {
|
||||||
|
if (!response.ok)
|
||||||
|
{
|
||||||
|
setManga([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Api.mangaWithIDsCreate(response.data).then(response => {
|
||||||
|
if (response.ok)
|
||||||
|
setManga(response.data);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}, [Api]);
|
}, [Api]);
|
||||||
|
|
||||||
@@ -35,7 +49,7 @@ export default function App () {
|
|||||||
return (
|
return (
|
||||||
<ApiContext.Provider value={Api}>
|
<ApiContext.Provider value={Api}>
|
||||||
<MangaConnectorContext.Provider value={mangaConnectors}>
|
<MangaConnectorContext.Provider value={mangaConnectors}>
|
||||||
<MangaContext.Provider value={{GetManga: GetManga}}>
|
<MangaContext.Provider value={manga}>
|
||||||
<Sheet className={"app"}>
|
<Sheet className={"app"}>
|
||||||
<Header>
|
<Header>
|
||||||
<Settings setApiUri={setApiUri} />
|
<Settings setApiUri={setApiUri} />
|
||||||
|
@@ -4,10 +4,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.manga-cover-blur {
|
.manga-cover-blur {
|
||||||
background: linear-gradient(to bottom, rgba(0,0,0,0.8), rgba(0,0,0,0.2) 75%);
|
background: linear-gradient(135deg, rgba(245, 169, 184, 0.9) 20%, rgba(91, 206, 250, 0.6));
|
||||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);)
|
||||||
backdrop-filter: blur(9px);
|
backdrop-filter: blur(6px);
|
||||||
-webkit-backdrop-filter: blur(9px);
|
-webkit-backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.manga-card-badge-icon {
|
.manga-card-badge-icon {
|
||||||
|
@@ -13,18 +13,16 @@ import {
|
|||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import {Manga} from "../../apiClient/data-contracts.ts";
|
import {Manga} from "../../apiClient/data-contracts.ts";
|
||||||
import {Dispatch, SetStateAction, useContext, useState} from "react";
|
import {Dispatch, SetStateAction, useContext, useState} from "react";
|
||||||
import {MangaContext} from "../../apiClient/MangaContext.tsx";
|
|
||||||
import "./MangaCard.css";
|
import "./MangaCard.css";
|
||||||
import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
|
import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||||
|
import {MangaContext} from "../../App.tsx";
|
||||||
|
|
||||||
export function MangaCardFromId({mangaId} : {mangaId: string}) {
|
export function MangaCardFromId({mangaId} : {mangaId: string}) {
|
||||||
const Mangas = useContext(MangaContext);
|
const mangas = useContext(MangaContext);
|
||||||
const [manga, setManga] = useState<Manga | undefined>(undefined);
|
const manga = mangas.find(manga => manga.key === mangaId);
|
||||||
|
|
||||||
Mangas.GetManga(mangaId).then(setManga);
|
|
||||||
|
|
||||||
return <MangaCard manga={manga} />
|
return <MangaCard manga={manga} />
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,15 @@
|
|||||||
import {useContext, useState} from "react";
|
import {useContext} from "react";
|
||||||
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
import {MangaCard} from "./MangaCard.tsx";
|
||||||
import {MangaCardFromId} from "./MangaCard.tsx";
|
|
||||||
import {Stack} from "@mui/joy";
|
import {Stack} from "@mui/joy";
|
||||||
import "./MangaList.css";
|
import "./MangaList.css";
|
||||||
|
import {MangaContext} from "../../App.tsx";
|
||||||
|
|
||||||
export default function MangaList (){
|
export default function MangaList (){
|
||||||
const Api = useContext(ApiContext);
|
const mangas = useContext(MangaContext);
|
||||||
|
|
||||||
const [mangaIds, setMangaIds] = useState<string[]>();
|
|
||||||
|
|
||||||
Api.mangaList().then((response) => {
|
|
||||||
if (!response.ok)
|
|
||||||
return;
|
|
||||||
setMangaIds(response.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"}>
|
<Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"}>
|
||||||
{mangaIds?.map(id => <MangaCardFromId key={id} mangaId={id} />)}
|
{mangas?.map(manga => <MangaCard key={manga.key} manga={manga} />)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -1,8 +1,27 @@
|
|||||||
import {ReactNode, useContext, useState} from "react";
|
import {ReactNode, useContext, useState} from "react";
|
||||||
import { ApiContext } from "../../apiClient/ApiContext";
|
import { ApiContext } from "../../apiClient/ApiContext";
|
||||||
import {Button, Input, Modal, ModalDialog, Tab, TabList, TabPanel, Tabs} from "@mui/joy";
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Stack,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs
|
||||||
|
} from "@mui/joy";
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts";
|
import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts";
|
||||||
|
import {Close, Done} from "@mui/icons-material";
|
||||||
|
|
||||||
|
enum LoadingState {
|
||||||
|
none,
|
||||||
|
loading,
|
||||||
|
success,
|
||||||
|
failure
|
||||||
|
}
|
||||||
|
|
||||||
export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) {
|
export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) {
|
||||||
|
|
||||||
@@ -25,11 +44,38 @@ export default function ({open, setOpen} : {open: boolean, setOpen: (open: boole
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NotificationConnectorTab({ value, children, add }: { value: string, children: ReactNode, add: (data: any) => void }) {
|
function NotificationConnectorTab({ value, children, add, state }: { value: string, children: ReactNode, add: (data: any) => void, state: LoadingState }) {
|
||||||
|
const StateIndicator = (state : LoadingState) : ReactNode => {
|
||||||
|
switch (state) {
|
||||||
|
case LoadingState.loading:
|
||||||
|
return (<CircularProgress />);
|
||||||
|
case LoadingState.failure:
|
||||||
|
return (<Close />);
|
||||||
|
case LoadingState.success:
|
||||||
|
return (<Done />);
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const StateColor = (state : LoadingState) => {
|
||||||
|
switch (state) {
|
||||||
|
case LoadingState.failure:
|
||||||
|
return "danger";
|
||||||
|
case LoadingState.success:
|
||||||
|
return "success";
|
||||||
|
default: return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const IsLoading = (state : LoadingState) : boolean => state === LoadingState.loading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabPanel value={value}>
|
<TabPanel value={value}>
|
||||||
{children}
|
<Stack spacing={1}>
|
||||||
<Button onClick={add}>Add</Button>
|
{children}
|
||||||
|
<Button onClick={add} endDecorator={StateIndicator(state)} loading={IsLoading(state)} disabled={IsLoading(state)} color={StateColor(state)}>Add</Button>
|
||||||
|
</Stack>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -37,9 +83,22 @@ function NotificationConnectorTab({ value, children, add }: { value: string, chi
|
|||||||
function Gotify() {
|
function Gotify() {
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext);
|
||||||
const [gotifyData, setGotifyData] = useState<GotifyRecord>({});
|
const [gotifyData, setGotifyData] = useState<GotifyRecord>({});
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const Add = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.notificationConnectorGotifyUpdate(gotifyData)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok)
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
})
|
||||||
|
.catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationConnectorTab value={"gotify"} add={() => Api.notificationConnectorGotifyUpdate(gotifyData)}>
|
<NotificationConnectorTab value={"gotify"} add={Add} state={loadingState}>
|
||||||
<Input placeholder={"Name"} value={gotifyData.name as string} onChange={(e) => setGotifyData({...gotifyData, name: e.target.value})} />
|
<Input placeholder={"Name"} value={gotifyData.name as string} onChange={(e) => setGotifyData({...gotifyData, name: e.target.value})} />
|
||||||
<Input placeholder={"https://[...]/message"} value={gotifyData.endpoint as string} onChange={(e) => setGotifyData({...gotifyData, endpoint: e.target.value})} />
|
<Input placeholder={"https://[...]/message"} value={gotifyData.endpoint as string} onChange={(e) => setGotifyData({...gotifyData, endpoint: e.target.value})} />
|
||||||
<Input placeholder={"Apptoken"} type={"password"} value={gotifyData.appToken as string} onChange={(e) => setGotifyData({...gotifyData, appToken: e.target.value})} />
|
<Input placeholder={"Apptoken"} type={"password"} value={gotifyData.appToken as string} onChange={(e) => setGotifyData({...gotifyData, appToken: e.target.value})} />
|
||||||
@@ -51,9 +110,22 @@ function Gotify() {
|
|||||||
function Ntfy() {
|
function Ntfy() {
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext);
|
||||||
const [ntfyData, setNtfyData] = useState<NtfyRecord>({});
|
const [ntfyData, setNtfyData] = useState<NtfyRecord>({});
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const Add = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.notificationConnectorNtfyUpdate(ntfyData)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok)
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
})
|
||||||
|
.catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationConnectorTab value={"ntfy"} add={() => Api.notificationConnectorNtfyUpdate(ntfyData)}>
|
<NotificationConnectorTab value={"ntfy"} add={Add} state={loadingState}>
|
||||||
<Input placeholder={"Name"} value={ntfyData.name as string} onChange={(e) => setNtfyData({...ntfyData, name: e.target.value})} />
|
<Input placeholder={"Name"} value={ntfyData.name as string} onChange={(e) => setNtfyData({...ntfyData, name: e.target.value})} />
|
||||||
<Input placeholder={"Endpoint"} value={ntfyData.endpoint as string} onChange={(e) => setNtfyData({...ntfyData, endpoint: e.target.value})} />
|
<Input placeholder={"Endpoint"} value={ntfyData.endpoint as string} onChange={(e) => setNtfyData({...ntfyData, endpoint: e.target.value})} />
|
||||||
<Input placeholder={"Topic"} value={ntfyData.topic as string} onChange={(e) => setNtfyData({...ntfyData, topic: e.target.value})} />
|
<Input placeholder={"Topic"} value={ntfyData.topic as string} onChange={(e) => setNtfyData({...ntfyData, topic: e.target.value})} />
|
||||||
@@ -67,9 +139,22 @@ function Ntfy() {
|
|||||||
function Pushover() {
|
function Pushover() {
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext);
|
||||||
const [pushoverData, setPushoverData] = useState<PushoverRecord>({});
|
const [pushoverData, setPushoverData] = useState<PushoverRecord>({});
|
||||||
|
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none);
|
||||||
|
|
||||||
|
const Add = () => {
|
||||||
|
setLoadingState(LoadingState.loading);
|
||||||
|
Api.notificationConnectorPushoverUpdate(pushoverData)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok)
|
||||||
|
setLoadingState(LoadingState.success);
|
||||||
|
else
|
||||||
|
setLoadingState(LoadingState.failure);
|
||||||
|
})
|
||||||
|
.catch(_ => setLoadingState(LoadingState.failure));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationConnectorTab value={"pushover"} add={() => Api.notificationConnectorPushoverUpdate(pushoverData)}>
|
<NotificationConnectorTab value={"pushover"} add={Add} state={loadingState}>
|
||||||
<Input placeholder={"Name"} value={pushoverData.name as string} onChange={(e) => setPushoverData({...pushoverData, name: e.target.value})} />
|
<Input placeholder={"Name"} value={pushoverData.name as string} onChange={(e) => setPushoverData({...pushoverData, name: e.target.value})} />
|
||||||
<Input placeholder={"User"} value={pushoverData.user as string} onChange={(e) => setPushoverData({...pushoverData, user: e.target.value})} />
|
<Input placeholder={"User"} value={pushoverData.user as string} onChange={(e) => setPushoverData({...pushoverData, user: e.target.value})} />
|
||||||
<Input placeholder={"AppToken"} type={"password"} value={pushoverData.appToken as string} onChange={(e) => setPushoverData({...pushoverData, appToken: e.target.value})} />
|
<Input placeholder={"AppToken"} type={"password"} value={pushoverData.appToken as string} onChange={(e) => setPushoverData({...pushoverData, appToken: e.target.value})} />
|
@@ -15,7 +15,7 @@ import {createContext, Dispatch, useContext, useEffect, useState} from "react";
|
|||||||
import {Article} from '@mui/icons-material';
|
import {Article} from '@mui/icons-material';
|
||||||
import {TrangaSettings} from "../../apiClient/data-contracts.ts";
|
import {TrangaSettings} from "../../apiClient/data-contracts.ts";
|
||||||
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
import {ApiContext} from "../../apiClient/ApiContext.tsx";
|
||||||
import NotificationConnectors from "./NotificationConnectors.tsx";
|
import NotificationConnectors from "./AddNotificationConnector.tsx";
|
||||||
|
|
||||||
export const SettingsContext = createContext<TrangaSettings>({});
|
export const SettingsContext = createContext<TrangaSettings>({});
|
||||||
|
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
import {createContext, useContext} from "react";
|
|
||||||
import {Manga} from "./data-contracts.ts";
|
|
||||||
import {ApiContext} from "./ApiContext.tsx";
|
|
||||||
|
|
||||||
const mangaPromises = new Map<string, Promise<Manga | undefined>>();
|
|
||||||
const mangas : Manga[] = [];
|
|
||||||
|
|
||||||
export const GetManga : (id: string) => Promise<Manga | undefined> = (id: string) => {
|
|
||||||
const API = useContext(ApiContext);
|
|
||||||
|
|
||||||
const promise = mangaPromises.get(id);
|
|
||||||
if(promise) return promise;
|
|
||||||
const p = new Promise<Manga | undefined>((resolve, reject) => {
|
|
||||||
let ret = mangas?.find(m => m.key == id);
|
|
||||||
if (ret) resolve(ret);
|
|
||||||
|
|
||||||
console.log(`Fetching manga ${id}`);
|
|
||||||
API.mangaDetail(id)
|
|
||||||
.then(result => {
|
|
||||||
if (!result.ok)
|
|
||||||
throw new Error(`Error fetching manga detail ${id}`);
|
|
||||||
mangas.push(result.data);
|
|
||||||
resolve(result.data);
|
|
||||||
}).catch(reject);
|
|
||||||
});
|
|
||||||
mangaPromises.set(id, p);
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MangaContext = createContext<{ GetManga: (id: string) => Promise<Manga | undefined> }>(
|
|
||||||
{
|
|
||||||
GetManga: GetManga
|
|
||||||
}
|
|
||||||
);
|
|
Reference in New Issue
Block a user