diff --git a/Website/App.tsx b/Website/App.tsx index 63af7b5..17cb48d 100644 --- a/Website/App.tsx +++ b/Website/App.tsx @@ -1,42 +1,50 @@ -import React, {ReactElement, useEffect} from 'react'; +import React, {EventHandler, ReactElement, useEffect, useState} from 'react'; import Footer from "./modules/Footer"; import Search from "./modules/Search"; import Header from "./modules/Header"; import MonitorJobsList from "./modules/MonitorJobsList"; -import './styles/Manga.css' +import './styles/index.css' export default function App(){ - const [content, setContent] = React.useState(); - - function ShowSearch() { - setContent(<> - - - ); - } + const [connected, setConnected] = React.useState(false); + const [showSearch, setShowSearch] = React.useState(false); + const [lastMangaListUpdate, setLastMangaListUpdate] = React.useState(new Date()); useEffect(() => { - setContent(

Testing connection to backend...

) getData('http://127.0.0.1:6531/v2/Ping').then((result) => { console.log(result); if(result === null){ - setContent(

No connection to backend

); + setConnected(false); }else{ - setContent(<> - - ) + setConnected(true); } }) }, []); + const JobsChanged : EventHandler = () => { + console.log("Updating Mangalist"); + setLastMangaListUpdate(new Date()); + } + return(
-
- {content} -
+
+ {connected + ? <> + {showSearch + ? <> + +
+ + : <>} + setShowSearch(true)} onJobsChanged={JobsChanged} + key={lastMangaListUpdate.getTime()}/> + + :

No connection to backend

} +
) } -export function getData (uri: string) : Promise { +export function getData(uri: string) : Promise { return fetch(uri, { method: 'GET', @@ -66,8 +74,10 @@ export function postData(uri: string, content: object) : Promise { body: JSON.stringify(content) }) .then(function(response){ - if(!response.ok) throw new Error("Could not fetch data"); - return response.json(); + if(!response.ok) + throw new Error("Could not fetch data"); + let json = response.json(); + return json.then((json) => json).catch(() => null); }) .catch(function(err){ console.error(`Error POSTing Data ${uri}\n${err}`); @@ -75,8 +85,8 @@ export function postData(uri: string, content: object) : Promise { }); } -export function deleteData(uri: string) { - fetch(uri, +export function deleteData(uri: string) : Promise { + return fetch(uri, { method: 'DELETE', headers : { @@ -84,6 +94,9 @@ export function deleteData(uri: string) { 'Accept': 'application/json' } }) + .then(() =>{ + return Promise.resolve(); + }) .catch(function(err){ console.error(`Error DELETEing Data ${uri}\n${err}`); return Promise.reject(); diff --git a/Website/modules/Job.tsx b/Website/modules/Job.tsx index ff9e16d..ae738f3 100644 --- a/Website/modules/Job.tsx +++ b/Website/modules/Job.tsx @@ -9,7 +9,9 @@ export class Job return getData("http://127.0.0.1:6531/v2/Jobs") .then((json) => { console.debug("Got all Jobs"); - return (json as string[]); + const ret = json as string[]; + console.debug(ret); + return (ret); }); } @@ -18,7 +20,9 @@ export class Job return getData("http://127.0.0.1:6531/v2/Jobs/Running") .then((json) => { console.debug("Got all running Jobs"); - return (json as string[]); + const ret = json as string[]; + console.debug(ret); + return (ret); }); } @@ -27,7 +31,9 @@ export class Job return getData("http://127.0.0.1:6531/v2/Jobs/Waiting") .then((json) => { console.debug("Got all waiting Jobs"); - return (json as string[]); + const ret = json as string[]; + console.debug(ret); + return (ret); }); } @@ -36,7 +42,9 @@ export class Job return getData("http://127.0.0.1:6531/v2/Jobs/Monitoring") .then((json) => { console.debug("Got all monitoring Jobs"); - return (json as string[]); + const ret = json as string[]; + console.debug(ret); + return (ret); }); } @@ -49,7 +57,9 @@ export class Job return getData(`http://127.0.0.1:6531/v2/Job/${jobId}`) .then((json) => { console.debug(`Got Job ${jobId}`); - return (json as IJob); + const ret = json as IJob; + console.debug(ret); + return (ret); }); } @@ -63,7 +73,9 @@ export class Job return getData(`http://127.0.0.1:6531/v2/Job?jobIds=${reqStr}`) .then((json) => { console.debug(`Got Jobs ${reqStr}`); - return (json as IJob[]); + const ret = json as IJob[]; + console.debug(ret); + return (ret); }); } @@ -72,11 +84,13 @@ export class Job return getData(`http://127.0.0.1:6531/v2/Job/${jobId}/Progress`) .then((json) => { console.debug(`Got Job ${jobId} Progress`); - return (json as IProgressToken); + const ret = json as IProgressToken; + console.debug(ret); + return (ret); }); } - static async CreateJob(internalId: string, jobType: string, interval: string): Promise { + static async CreateJob(internalId: string, jobType: string, interval: string): Promise { console.debug(`Creating Job for Manga ${internalId} at ${interval} interval`); let data = { internalId: internalId, @@ -85,19 +99,19 @@ export class Job return postData(`http://127.0.0.1:6531/v2/Job/Create/${jobType}`, data) .then((json) => { console.debug(`Created Job for Manga ${internalId} at ${interval} interval`); - return (json as IJob); + return null; }); } - static DeleteJob(jobId: string) { - deleteData(`http://127.0.0.1:6531/v2/Job/${jobId}`); + static DeleteJob(jobId: string) : Promise { + return deleteData(`http://127.0.0.1:6531/v2/Job/${jobId}`); } - static StartJob(jobId: string) { - postData(`http://127.0.0.1:6531/v2/Job/${jobId}/StartNow`, {}); + static StartJob(jobId: string) : Promise { + return postData(`http://127.0.0.1:6531/v2/Job/${jobId}/StartNow`, {}); } - static CancelJob(jobId: string) { - postData(`http://127.0.0.1:6531/v2/Job/${jobId}/Cancel`, {}); + static CancelJob(jobId: string) : Promise { + return postData(`http://127.0.0.1:6531/v2/Job/${jobId}/Cancel`, {}); } } \ No newline at end of file diff --git a/Website/modules/Manga.tsx b/Website/modules/Manga.tsx index ea6db5a..c7aea1d 100644 --- a/Website/modules/Manga.tsx +++ b/Website/modules/Manga.tsx @@ -8,7 +8,9 @@ export class Manga return getData("http://127.0.0.1:6531/v2/Mangas") .then((json) => { console.debug("Got all Manga"); - return (json as IManga[]); + const ret = json as IManga[]; + console.debug(ret); + return (ret); }); } @@ -17,7 +19,9 @@ export class Manga return await getData(`http://127.0.0.1:6531/v2/Manga/Search?title=${name}`) .then((json) => { console.debug(`Got Manga ${name}`); - return (json as IManga[]); + const ret = json as IManga[]; + console.debug(ret); + return (ret); }); } @@ -26,7 +30,9 @@ export class Manga return await getData(`http://127.0.0.1:6531/v2/Manga/${internalId}`) .then((json) => { console.debug(`Got Manga ${internalId}`); - return (json as IManga); + const ret = json as IManga; + console.debug(ret); + return (ret); }); } @@ -35,7 +41,9 @@ export class Manga return await getData(`http://127.0.0.1:6531/v2/Manga?internalIds=${internalIds.join(",")}`) .then((json) => { console.debug(`Got Manga ${internalIds.join(",")}`); - return (json as IManga[]); + const ret = json as IManga[]; + console.debug(ret); + return (ret); }); } diff --git a/Website/modules/MonitorJobsList.tsx b/Website/modules/MonitorJobsList.tsx index 0f4cf04..1fc4bca 100644 --- a/Website/modules/MonitorJobsList.tsx +++ b/Website/modules/MonitorJobsList.tsx @@ -1,25 +1,17 @@ -import React, {MouseEventHandler, ReactElement, useEffect} from 'react'; +import React, {EventHandler, MouseEventHandler, ReactElement, useEffect, useState} from 'react'; import {Job} from './Job'; import '../styles/monitorMangaList.css'; import IJob from "./interfaces/IJob"; -import IManga, {HTMLFromIManga} from "./interfaces/IManga"; +import IManga, {CoverCard} from "./interfaces/IManga"; import {Manga} from './Manga'; +import '../styles/MangaCoverCard.css' -export default function MonitorJobsList({onStartSearch} : {onStartSearch() : void}){ - const [MonitoringJobs, setMonitoringJobs] = React.useState([]); - const [AllManga, setAllManga] = React.useState([]); - - function UpdateMonitoringJobsList(){ - Job.GetMonitoringJobs() - .then((jobs) => { - if(jobs.length > 0) - return Job.GetJobs(jobs) - return []; - }) - .then((jobs) => setMonitoringJobs(jobs)); - } +export default function MonitorJobsList({onStartSearch, onJobsChanged} : {onStartSearch() : void, onJobsChanged: EventHandler}) { + const [MonitoringJobs, setMonitoringJobs] = useState([]); + const [AllManga, setAllManga] = useState([]); useEffect(() => { + console.debug("Updating display list."); //Remove all Manga that are not associated with a Job setAllManga(AllManga.filter(manga => MonitoringJobs.find(job => job.mangaInternalId == manga.internalId))); //Fetch Manga that are missing (from Jobs) @@ -36,9 +28,20 @@ export default function MonitorJobsList({onStartSearch} : {onStartSearch() : voi UpdateMonitoringJobsList(); }, []); - const DeleteJob:MouseEventHandler = (e) => { + const DeleteJob : MouseEventHandler = (e) => { const jobId = e.currentTarget.id; - Job.DeleteJob(jobId); + Job.DeleteJob(jobId).then(() => onJobsChanged(jobId)); + } + + function UpdateMonitoringJobsList(){ + console.debug("Updating MonitoringJobsList"); + Job.GetMonitoringJobs() + .then((jobs) => { + if(jobs.length > 0) + return Job.GetJobs(jobs) + return []; + }) + .then((jobs) => setMonitoringJobs(jobs)); } function StartSearchMangaEntry() : ReactElement { @@ -61,7 +64,7 @@ export default function MonitorJobsList({onStartSearch} : {onStartSearch() : voi if (job === undefined || job == null) return
Error. Could not find matching job for {manga.internalId}
return
- {HTMLFromIManga(manga)} + {CoverCard(manga)} {job.id}
; diff --git a/Website/modules/Search.tsx b/Website/modules/Search.tsx index 64969ae..253ed21 100644 --- a/Website/modules/Search.tsx +++ b/Website/modules/Search.tsx @@ -1,12 +1,13 @@ -import React, { ChangeEventHandler, MouseEventHandler, useEffect, useState} from 'react'; +import React, {ChangeEventHandler, EventHandler, MouseEventHandler, useEffect, useState} from 'react'; import {MangaConnector} from "./MangaConnector"; import {Job} from "./Job"; import IMangaConnector from "./interfaces/IMangaConnector"; import {isValidUri} from "../App"; -import IManga, {HTMLFromIManga} from "./interfaces/IManga"; +import IManga, {SearchResult} from "./interfaces/IManga"; import '../styles/search.css'; +import '../styles/MangaSearchResult.css' -export default function Search(){ +export default function Search({onJobsChanged} : {onJobsChanged: EventHandler}) { const [mangaConnectors, setConnectors] = useState(); const [selectedConnector, setSelectedConnector] = useState(); const [selectedLanguage, setSelectedLanguage] = useState(); @@ -88,28 +89,25 @@ export default function Search(){ return (
-
+
{searchResults === undefined - ?

No Results yet

- : searchResults.map(result =>
- {HTMLFromIManga(result)} - -
)} + ?

+ : searchResults.map(result => SearchResult(result, onJobsChanged))}
) } \ No newline at end of file diff --git a/Website/modules/interfaces/IManga.tsx b/Website/modules/interfaces/IManga.tsx index d6acebf..9fd5214 100644 --- a/Website/modules/interfaces/IManga.tsx +++ b/Website/modules/interfaces/IManga.tsx @@ -1,7 +1,10 @@ import IMangaConnector from "./IMangaConnector"; import KeyValuePair from "./KeyValuePair"; import {Manga} from "../Manga"; -import {ReactElement} from "react"; +import React, {ChangeEventHandler, EventHandler, ReactElement} from "react"; +import {Job} from "../Job"; +import Icon from '@mdi/react'; +import { mdiTagTextOutline, mdiAccountEdit } from '@mdi/js'; export default interface IManga{ "sortName": string, @@ -36,7 +39,7 @@ function ReleaseStatusFromNumber(n: number): string { return ""; } -export function HTMLFromIManga(manga: IManga) : ReactElement { +export function CoverCard(manga: IManga) : ReactElement { return(
@@ -46,4 +49,23 @@ export function HTMLFromIManga(manga: IManga) : ReactElement {

{manga.sortName}

); +} + +export function SearchResult(manga: IManga, jobsChanged: EventHandler) : ReactElement { + return( +
+ +

{manga.mangaConnector.name}

+
+

{manga.sortName}

+
    + {manga.authors.map(author =>
  • {author}
  • )} + {manga.tags.map(tag =>
  • {tag}
  • )} +
+

{manga.description}

+ +
); } \ No newline at end of file diff --git a/Website/styles/Manga.css b/Website/styles/MangaCoverCard.css similarity index 100% rename from Website/styles/Manga.css rename to Website/styles/MangaCoverCard.css diff --git a/Website/styles/MangaSearchResult.css b/Website/styles/MangaSearchResult.css new file mode 100644 index 0000000..4430ab8 --- /dev/null +++ b/Website/styles/MangaSearchResult.css @@ -0,0 +1,88 @@ +.SearchResult { + background-color: var(--second-background-color); + border-radius: 2px; + padding: 5px 5px 9px 5px; + position: relative; + max-width: 100%; + width: fit-content; + height: 328px; + display: grid; + grid-template-columns: 220px 600px 50px; + grid-template-rows: 40px 40px 200px auto; + column-gap: 10px; + grid-template-areas: + "cover header header" + "cover alltags alltags" + "cover description description" + "cover footer button"; +} + +.SearchResult p { + margin: 2px 0; +} + +.SearchResult > img { + grid-area: cover; + position: relative; + height: 100%; + width: 100%; + z-index: 0; + border: 2px solid var(--primary-color); + border-radius: 4px; +} + +.SearchResult > .connector-name { + grid-area: cover; + position: absolute; + z-index: 1; + left: 2px; + top: 2px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + width: 100%; + background-color: var(--accent-color); + margin: 0; + padding: 2px 0; + text-align: center; + color: var(--secondary-color); +} + +.SearchResult > .Manga-status { + grid-area: header; +} + +.SearchResult > .Manga-name { + grid-area: header; + color: black; +} + +.SearchResult > .Manga-tags { + grid-area: alltags; + color: white; + padding: 0; + margin: 0; +} + +.SearchResult > ul > li { + display: inline; + margin: 0 2px; + padding: 5px; + font-size: 10pt; +} + +.SearchResult .Manga-author { + background-color: green; +} + +.SearchResult .Manga-tag { + background-color: blue; +} + +.SearchResult > .Manga-description { + grid-area: description; + color: black; +} + +.SearchResult > .Manga-AddButton { + grid-area: button; +} \ No newline at end of file diff --git a/Website/styles/search.css b/Website/styles/search.css index e06b205..adece36 100644 --- a/Website/styles/search.css +++ b/Website/styles/search.css @@ -1,3 +1,48 @@ -.searchResult{ - background-color: var(--second-background-color); +#SearchBox{ + display: flex; + align-content: center; + justify-content: center; + margin: 10px 0; +} + +#SearchResults { + max-width: 100vw; +} + +#SearchBox select, #SearchBox button, #SearchBox input { + border-color: var(--accent-color); + border-style: solid; + border-width: 0; + border-bottom-width: 2px; + border-top-width: 2px; + padding: 2px 5px; + font-size: 12pt; +} + +#Searchbox-Manganame { + border-bottom-left-radius: 2px; + border-top-left-radius: 2px; + border-left-width: 2px; + border-right-width: 0; + width: 300px; +} + +#Searchbox-connector { + border-left-width: 0; + border-right-width: 0; + width: max-content; +} + +#Searchbox-language { + border-left-width: 0; + border-right-width: 0; + width: 90px; +} + +#Searchbox-button { + border-bottom-right-radius: 2px; + border-top-right-radius: 2px; + border-left-width: 0; + border-right-width: 2px; + width: 90px; } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 340018a..3439150 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,8 @@ "packages": { "": { "devDependencies": { + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@types/react": "^18.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -403,6 +405,23 @@ "node": ">=12" } }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@mdi/react": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@mdi/react/-/react-1.6.1.tgz", + "integrity": "sha512-4qZeDcluDFGFTWkHs86VOlHkm6gnKaMql13/gpIcUQ8kzxHgpj31NuCkD8abECVfbULJ3shc7Yt4HJ6Wu6SN4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", @@ -760,6 +779,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -796,6 +825,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -823,6 +864,13 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.24.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", diff --git a/package.json b/package.json index 4d2fd4e..854da5d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,7 @@ { "devDependencies": { + "@mdi/js": "^7.4.47", + "@mdi/react": "^1.6.1", "@types/react": "^18.2.0", "react": "^18.3.1", "react-dom": "^18.3.1",