mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-06-15 08:17:53 +02:00
Add Header, Footer, Basic Search
This commit is contained in:
42
Website/App.tsx
Normal file
42
Website/App.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import Footer from "./modules/Footer";
|
||||
import Search from "./modules/Search";
|
||||
import Header from "./modules/Header";
|
||||
|
||||
export default function App(){
|
||||
// @ts-ignore
|
||||
const content = <div>
|
||||
<Header />
|
||||
<Search />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
return(content)
|
||||
}
|
||||
|
||||
export function getData (uri: string) : Promise<object> {
|
||||
return fetch(uri,
|
||||
{
|
||||
headers : {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(function(response){
|
||||
if(!response.ok) throw new Error("Could not fetch data");
|
||||
return response.json();
|
||||
})
|
||||
.catch(function(err){
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
export function isValidUri(uri: string) : boolean{
|
||||
try {
|
||||
new URL(uri);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
13
Website/index.html
Normal file
13
Website/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tranga</title>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="stylesheet" href="styles/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
7
Website/index.jsx
Normal file
7
Website/index.jsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
const domNode = document.getElementById('app');
|
||||
const root = createRoot(domNode);
|
||||
root.render(<App />);
|
8
Website/modules/Footer.tsx
Normal file
8
Website/modules/Footer.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Footer(){
|
||||
return (
|
||||
<footer>
|
||||
<p id="madeWith">Made with Blåhaj 🦈</p>
|
||||
</footer>)
|
||||
}
|
11
Website/modules/Header.tsx
Normal file
11
Website/modules/Header.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Header(){
|
||||
return (
|
||||
<header>
|
||||
<div id="titlebox">
|
||||
<img alt="website image is Blahaj" src="media/blahaj.png"/>
|
||||
<span>Tranga</span>
|
||||
</div>
|
||||
</header>)
|
||||
}
|
51
Website/modules/Manga.tsx
Normal file
51
Website/modules/Manga.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import IManga from './interfaces/IManga';
|
||||
import { getData } from '../App';
|
||||
|
||||
export class Manga
|
||||
{
|
||||
static async GetAllManga(): Promise<IManga[]> {
|
||||
let manga: IManga[] = [];
|
||||
console.debug("Getting all Manga");
|
||||
return getData("http://127.0.0.1:6531/v2/Mangas")
|
||||
.then((json) => {
|
||||
console.debug("Got all Manga");
|
||||
return (json as IManga[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async SearchManga(name: string): Promise<IManga[]> {
|
||||
console.debug(`Getting Manga ${name} from all Connectors`);
|
||||
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[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaById(internalId: string): Promise<IManga> {
|
||||
console.debug(`Getting Manga ${internalId}`);
|
||||
return await getData(`http://127.0.0.1:6531/v2/Manga/${internalId}`)
|
||||
.then((json) => {
|
||||
console.debug(`Got Manga ${internalId}`);
|
||||
return (json as IManga);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaByIds(internalIds: string[]): Promise<IManga[]> {
|
||||
console.debug(`Getting Mangas ${internalIds.join(",")}`);
|
||||
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[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaCoverUrl(internalId: string): Promise<string> {
|
||||
console.debug(`Getting Manga Cover-Url ${internalId}`);
|
||||
return await getData(`http://127.0.0.1:6531/v2/Manga/${internalId}/Cover`)
|
||||
.then((json) => {
|
||||
console.debug(`Got Cover-Url of Manga ${internalId}`);
|
||||
return (json.toString());
|
||||
});
|
||||
}
|
||||
}
|
33
Website/modules/MangaConnector.tsx
Normal file
33
Website/modules/MangaConnector.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import IMangaConnector from './interfaces/IMangaConnector';
|
||||
import IManga from './interfaces/IManga';
|
||||
import { getData } from '../App';
|
||||
|
||||
export class MangaConnector
|
||||
{
|
||||
static async GetAllConnectors(): Promise<IMangaConnector[]> {
|
||||
console.debug("Getting all MangaConnectors");
|
||||
return getData("http://127.0.0.1:6531/v2/Connector/Types")
|
||||
.then((json) => {
|
||||
console.debug("Got all MangaConnectors");
|
||||
return (json as IMangaConnector[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaFromConnectorByTitle(connector: IMangaConnector, name: string): Promise<IManga[]> {
|
||||
console.debug(`Getting Manga ${name}`);
|
||||
return await getData(`http://127.0.0.1:6531/v2/Connector/${connector.name}/GetManga?title=${name}`)
|
||||
.then((json) => {
|
||||
console.debug(`Got Manga ${name}`);
|
||||
return (json as IManga[]);
|
||||
});
|
||||
}
|
||||
|
||||
static async GetMangaFromConnectorByUrl(connector: IMangaConnector, url: string): Promise<IManga> {
|
||||
console.debug(`Getting Manga ${url}`);
|
||||
return await getData(`http://127.0.0.1:6531/v2/Connector/${connector.name}/GetManga?url=${url}`)
|
||||
.then((json) => {
|
||||
console.debug(`Got Manga ${url}`);
|
||||
return (json as IManga);
|
||||
});
|
||||
}
|
||||
}
|
110
Website/modules/Search.tsx
Normal file
110
Website/modules/Search.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import React, { ChangeEventHandler, MouseEventHandler, useEffect, useState} from 'react';
|
||||
import {MangaConnector} from "./MangaConnector";
|
||||
import IMangaConnector from "./interfaces/IMangaConnector";
|
||||
import {isValidUri} from "../App";
|
||||
import IManga, {HTMLFromIManga} from "./interfaces/IManga";
|
||||
|
||||
export default function Search(){
|
||||
const [mangaConnectors, setConnectors] = useState<IMangaConnector[]>();
|
||||
const [selectedConnector, setSelectedConnector] = useState<IMangaConnector>();
|
||||
const [selectedLanguage, setSelectedLanguage] = useState<string>();
|
||||
const [searchBoxValue, setSearchBoxValue] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<IManga[]>();
|
||||
|
||||
|
||||
const pattern = /https:\/\/([a-z0-9.]+\.[a-z0-9]{2,})(?:\/.*)?/i
|
||||
|
||||
useEffect(() => {
|
||||
if(mangaConnectors === undefined) {
|
||||
MangaConnector.GetAllConnectors().then(setConnectors);
|
||||
return;
|
||||
}
|
||||
}, [mangaConnectors]);
|
||||
|
||||
const selectedConnectorChanged : ChangeEventHandler<HTMLSelectElement> = (event) => {
|
||||
event.preventDefault();
|
||||
if(mangaConnectors === undefined)
|
||||
return;
|
||||
const selectedConnector = mangaConnectors.find((con: IMangaConnector) => con.name == event.target.value);
|
||||
if(selectedConnector === undefined)
|
||||
return;
|
||||
setSelectedConnector(selectedConnector);
|
||||
setSelectedLanguage(selectedConnector.SupportedLanguages[0]);
|
||||
}
|
||||
|
||||
const searchBoxValueChanged : ChangeEventHandler<HTMLInputElement> = (event) => {
|
||||
if(mangaConnectors === undefined)
|
||||
return;
|
||||
var str : string = event.target.value;
|
||||
setSearchBoxValue(str);
|
||||
if(isValidUri(str))
|
||||
setSelectedConnector(undefined);
|
||||
const match = str.match(pattern);
|
||||
if(match === null)
|
||||
return;
|
||||
let baseUri = match[1];
|
||||
const selectCon = mangaConnectors.find((con: IMangaConnector) => {
|
||||
let found = con.BaseUris.find(uri => uri == baseUri);
|
||||
return found;
|
||||
});
|
||||
if(selectCon != undefined){
|
||||
setSelectedConnector(selectCon);
|
||||
setSelectedLanguage(selectCon.SupportedLanguages[0]);
|
||||
}
|
||||
}
|
||||
|
||||
const ExecuteSearch : MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||
if(searchBoxValue.length < 1 || selectedConnector === undefined || selectedLanguage === ""){
|
||||
console.error("Tried initiating search while not all fields where submitted.")
|
||||
return;
|
||||
}
|
||||
console.debug(`Searching Name: ${searchBoxValue} Connector: ${selectedConnector.name} Language: ${selectedLanguage}`);
|
||||
if(isValidUri(searchBoxValue) && !selectedConnector.BaseUris.find((uri: string) => {
|
||||
const match = searchBoxValue.match(pattern);
|
||||
if(match === null)
|
||||
return false;
|
||||
return match[1] == uri
|
||||
}))
|
||||
{
|
||||
console.error("URL in Searchbox detected, but does not match selected connector.");
|
||||
return;
|
||||
}
|
||||
if(!isValidUri(searchBoxValue)){
|
||||
MangaConnector.GetMangaFromConnectorByTitle(selectedConnector, searchBoxValue)
|
||||
.then((mangas: IManga[]) => {
|
||||
setSearchResults(mangas);
|
||||
});
|
||||
}else{
|
||||
MangaConnector.GetMangaFromConnectorByUrl(selectedConnector, searchBoxValue)
|
||||
.then((manga: IManga) => {
|
||||
setSearchResults([manga]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const changeSelectedLanguage : ChangeEventHandler<HTMLSelectElement> = (event) => setSelectedLanguage(event.target.value);
|
||||
|
||||
return (<div>
|
||||
<div id="SearchBox">
|
||||
<input type="text" placeholder="Manganame" onChange={searchBoxValueChanged}></input>
|
||||
<select value={selectedConnector === undefined ? "" : selectedConnector.name} onChange={selectedConnectorChanged}>
|
||||
<option value="" disabled hidden>Select</option>
|
||||
{mangaConnectors === undefined
|
||||
? <option value="Loading">Loading</option>
|
||||
: mangaConnectors.map(con => <option value={con.name} key={con.name}>{con.name}</option>)}
|
||||
</select>
|
||||
<select onChange={changeSelectedLanguage} value={selectedLanguage === null ? "" : selectedLanguage}>
|
||||
{selectedConnector === undefined
|
||||
? <option value="" disabled hidden>Select Connector</option>
|
||||
: selectedConnector.SupportedLanguages.map(language => <option value={language}
|
||||
key={language}>{language}</option>)}
|
||||
</select>
|
||||
<button type="submit" onClick={ExecuteSearch}>Search</button>
|
||||
</div>
|
||||
<div>
|
||||
{searchResults === undefined
|
||||
? <p>No Results yet</p>
|
||||
: searchResults.map(result => HTMLFromIManga(result))}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
34
Website/modules/interfaces/IManga.tsx
Normal file
34
Website/modules/interfaces/IManga.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import IMangaConnector from "./IMangaConnector";
|
||||
import KeyValuePair from "./KeyValuePair";
|
||||
import {Manga} from "../Manga";
|
||||
import {ReactElement} from "react";
|
||||
|
||||
export default interface IManga{
|
||||
"sortName": string,
|
||||
"authors": string[],
|
||||
"altTitles": KeyValuePair[],
|
||||
"description": string,
|
||||
"tags": string[],
|
||||
"coverUrl": string,
|
||||
"coverFileNameInCache": string,
|
||||
"links": KeyValuePair[],
|
||||
"year": number,
|
||||
"originalLanguage": string,
|
||||
"releaseStatus": number,
|
||||
"folderName": string,
|
||||
"publicationId": string,
|
||||
"internalId": string,
|
||||
"ignoreChaptersBelow": number,
|
||||
"latestChapterDownloaded": number,
|
||||
"latestChapterAvailable": number,
|
||||
"websiteUrl": string,
|
||||
"mangaConnector": IMangaConnector
|
||||
}
|
||||
|
||||
export function HTMLFromIManga(manga: IManga) : ReactElement {
|
||||
return (<div className="Manga" key={manga.internalId}>
|
||||
<p>{manga.sortName}</p>
|
||||
<p>Description: {manga.description}</p>
|
||||
<p>MangaConnector: {manga.mangaConnector.name}</p>
|
||||
</div>)
|
||||
}
|
5
Website/modules/interfaces/IMangaConnector.tsx
Normal file
5
Website/modules/interfaces/IMangaConnector.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default interface IMangaConnector {
|
||||
SupportedLanguages: string[];
|
||||
name: string;
|
||||
BaseUris: string[];
|
||||
}
|
4
Website/modules/interfaces/KeyValuePair.tsx
Normal file
4
Website/modules/interfaces/KeyValuePair.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default interface KeyValuePair {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
77
Website/styles/index.css
Normal file
77
Website/styles/index.css
Normal file
@ -0,0 +1,77 @@
|
||||
:root{
|
||||
--background-color: #030304;
|
||||
--second-background-color: white;
|
||||
--primary-color: #f5a9b8;
|
||||
--secondary-color: #5bcefa;
|
||||
--blur-background: rgba(245, 169, 184, 0.58);
|
||||
--accent-color: #fff;
|
||||
/* --primary-color: green;
|
||||
--secondary-color: gold;
|
||||
--blur-background: rgba(86, 131, 36, 0.8);
|
||||
--accent-color: olive; */
|
||||
--topbar-height: 60px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100vh;
|
||||
background-color: var(--background-color);
|
||||
font-family: "Inter", sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--topbar-height);
|
||||
background-color: var(--secondary-color);
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 20px black;
|
||||
}
|
||||
|
||||
header > #titlebox {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin: 0 0 0 40px;
|
||||
height: 100%;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
}
|
||||
|
||||
header > #titlebox > span{
|
||||
cursor: default;
|
||||
font-size: 24pt;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(150deg, var(--primary-color), var(--accent-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
header > #titlebox > img {
|
||||
height: 100%;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--primary-color);
|
||||
align-content: center;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#madeWith {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
margin-right: 20px;
|
||||
cursor: url("Website/media/blahaj.png"), grab;
|
||||
}
|
Reference in New Issue
Block a user