mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-09-10 11:58:20 +02:00
MangaDetail
This commit is contained in:
@@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { ApiConfig } from './api/http-client.ts'
|
import { ApiConfig } from './api/http-client.ts'
|
||||||
import MangaProvider from './contexts/MangaContext.tsx'
|
import MangaProvider from './contexts/MangaContext.tsx'
|
||||||
import MangaList from './Components/Mangas/MangaList.tsx'
|
import MangaList from './Components/Mangas/MangaList.tsx'
|
||||||
import Search from './Search.tsx'
|
import {Search} from './Search.tsx'
|
||||||
import MangaConnectorProvider from './contexts/MangaConnectorContext.tsx'
|
import MangaConnectorProvider from './contexts/MangaConnectorContext.tsx'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
@@ -6,51 +6,30 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/joy'
|
} from '@mui/joy'
|
||||||
import { EventHandler, ReactNode, useContext, useEffect, useState } from 'react'
|
import { EventHandler, ReactNode, useContext } from 'react'
|
||||||
import './MangaCard.css'
|
import './MangaCard.css'
|
||||||
import MangaConnectorIcon from './MangaConnectorIcon.tsx'
|
import MangaConnectorIcon from './MangaConnectorIcon.tsx'
|
||||||
import { Manga, MinimalManga } from '../../api/data-contracts.ts'
|
import { Manga, MinimalManga } from '../../api/data-contracts.ts'
|
||||||
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
|
|
||||||
export default function MangaCard({
|
export default function MangaCard(props: MangaCardProps): ReactNode {
|
||||||
mangaDetail,
|
const Api = useContext(ApiContext);
|
||||||
key,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
mangaDetail?: Manga | MinimalManga
|
|
||||||
key?: string
|
|
||||||
onClick?: EventHandler<any>
|
|
||||||
}): ReactNode {
|
|
||||||
const Api = useContext(ApiContext)
|
|
||||||
|
|
||||||
const [manga, setManga] = useState<Manga | MinimalManga | undefined>(
|
|
||||||
mangaDetail
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!key) return
|
|
||||||
Api.mangaDetail(key).then((data) => {
|
|
||||||
if (data.ok) {
|
|
||||||
setManga(data.data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, [Api, key])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent={manga?.mangaConnectorIds.map((id) => (
|
badgeContent={props.manga?.mangaConnectorIds.map((id) => (
|
||||||
<MangaConnectorIcon mangaConnectorName={id.mangaConnectorName} />
|
<MangaConnectorIcon mangaConnectorName={id.mangaConnectorName} />
|
||||||
))}
|
))}
|
||||||
className={'manga-card-badge'}
|
className={'manga-card-badge'}
|
||||||
>
|
>
|
||||||
<Card className={'manga-card'} onClick={onClick}>
|
<Card className={'manga-card'} onClick={props.onClick}>
|
||||||
<CardCover className={'manga-card-cover'}>
|
<CardCover className={'manga-card-cover'}>
|
||||||
<img src={manga && manga.key != "Search" ? `${Api.baseUrl}/v2/Manga/${manga?.key}/Cover` : '/blahaj.png'} />
|
<img src={props.manga && props.manga.key != "Search" ? `${Api.baseUrl}/v2/Manga/${props.manga?.key}/Cover` : '/blahaj.png'} />
|
||||||
</CardCover>
|
</CardCover>
|
||||||
<CardCover className={'manga-card-cover-blur'} />
|
<CardCover className={'manga-card-cover-blur'} />
|
||||||
<CardContent className={'manga-card-content'}>
|
<CardContent className={'manga-card-content'}>
|
||||||
<Typography level={'h4'}>
|
<Typography level={'h4'}>
|
||||||
{manga?.name ?? (
|
{props.manga?.name ?? (
|
||||||
<Skeleton>{stringWithRandomLength()}</Skeleton>
|
<Skeleton>{stringWithRandomLength()}</Skeleton>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -60,6 +39,11 @@ export default function MangaCard({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MangaCardProps {
|
||||||
|
manga?: Manga | MinimalManga
|
||||||
|
onClick?: EventHandler<any>
|
||||||
|
}
|
||||||
|
|
||||||
const stringWithRandomLength = (): string => {
|
const stringWithRandomLength = (): string => {
|
||||||
return 'wow'
|
return 'wow'
|
||||||
}
|
}
|
||||||
|
@@ -17,7 +17,6 @@ export default function MangaConnectorIcon({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(mangaConnector, mangaConnectorName);
|
|
||||||
if (mangaConnector) {
|
if (mangaConnector) {
|
||||||
setConnector(mangaConnector)
|
setConnector(mangaConnector)
|
||||||
return;
|
return;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Stack } from '@mui/joy'
|
import { Stack } from '@mui/joy'
|
||||||
import './MangaList.css'
|
import './MangaList.css'
|
||||||
import { ReactNode, useContext, useEffect, useState } from 'react'
|
import {Dispatch, ReactNode, useContext, useEffect, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
Manga,
|
Manga,
|
||||||
MangaReleaseStatus,
|
MangaReleaseStatus,
|
||||||
@@ -29,7 +29,7 @@ export default function MangaList({
|
|||||||
<MangaCardList manga={downloadingManga}>
|
<MangaCardList manga={downloadingManga}>
|
||||||
<MangaCard
|
<MangaCard
|
||||||
onClick={openSearch}
|
onClick={openSearch}
|
||||||
mangaDetail={{
|
manga={{
|
||||||
name: 'Search',
|
name: 'Search',
|
||||||
description: 'Search for a new Manga',
|
description: 'Search for a new Manga',
|
||||||
releaseStatus: MangaReleaseStatus.Continuing,
|
releaseStatus: MangaReleaseStatus.Continuing,
|
||||||
@@ -41,13 +41,7 @@ export default function MangaList({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MangaCardList({
|
export function MangaCardList(props: MangaCardListProps): ReactNode {
|
||||||
manga,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
manga: (Manga | MinimalManga)[]
|
|
||||||
children?: ReactNode
|
|
||||||
}): ReactNode {
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
className={'manga-list'}
|
className={'manga-list'}
|
||||||
@@ -60,13 +54,16 @@ export function MangaCardList({
|
|||||||
justifyItems: 'space-between',
|
justifyItems: 'space-between',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{props.children}
|
||||||
{manga.map((m) => (
|
{props.manga.map((m) => (
|
||||||
<>
|
<MangaCard key={m.key} manga={m} onClick={() => { if(props.mangaOnClick) props.mangaOnClick(m); } } />
|
||||||
<MangaCard mangaDetail={m} />
|
|
||||||
<span style={{ flexGrow: 1 }} />
|
|
||||||
</>
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MangaCardListProps {
|
||||||
|
manga: (Manga | MinimalManga)[]
|
||||||
|
children?: ReactNode
|
||||||
|
mangaOnClick?: Dispatch<Manga | MinimalManga>
|
||||||
|
}
|
||||||
|
59
tranga-website/src/MangaDetail.tsx
Normal file
59
tranga-website/src/MangaDetail.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {Manga} from "./api/data-contracts.ts";
|
||||||
|
import {Dispatch, ReactNode, useContext, useEffect, useState} from "react";
|
||||||
|
import {Card, CardCover, Chip, Modal, ModalDialog, Stack, Typography, useTheme} from "@mui/joy";
|
||||||
|
import ModalClose from "@mui/joy/ModalClose";
|
||||||
|
import {ApiContext} from "./contexts/ApiContext.tsx";
|
||||||
|
import {MangaContext} from "./contexts/MangaContext.tsx";
|
||||||
|
import './Components/Mangas/MangaCard.css'
|
||||||
|
import MarkdownPreview from "@uiw/react-markdown-preview";
|
||||||
|
|
||||||
|
export default function MangaDetail (props: MangaDetailProps) : ReactNode {
|
||||||
|
const Api = useContext(ApiContext);
|
||||||
|
const Manga = useContext(MangaContext);
|
||||||
|
|
||||||
|
const [manga, setManga] = useState<Manga | undefined>(props.manga)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!props.open) return;
|
||||||
|
if (!props.mangaKey) return;
|
||||||
|
if (props.manga != undefined) return;
|
||||||
|
Manga.GetManga(props.mangaKey).then(setManga);
|
||||||
|
}, [Api, props]);
|
||||||
|
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={props.open} onClose={() => props.setOpen(false)}>
|
||||||
|
<ModalDialog>
|
||||||
|
<ModalClose />
|
||||||
|
<div style={{display: 'flex', flexWrap: 'wrap', flexDirection: 'row'}}>
|
||||||
|
<Typography level={"h3"} sx={{width: '100%'}}>{manga?.name}</Typography>
|
||||||
|
<Card className={'manga-card'}>
|
||||||
|
<CardCover className={'manga-card-cover'}>
|
||||||
|
<img src={manga ? `${Api.baseUrl}/v2/Manga/${manga.key}/Cover` : '/blahaj.png'} />
|
||||||
|
</CardCover>
|
||||||
|
</Card>
|
||||||
|
<Stack direction={'column'} gap={2} sx={{maxWidth: 'calc(100% - 230px)', margin: '0 5px'}}>
|
||||||
|
<Stack direction={'row'} gap={0.5} flexWrap={'wrap'}>
|
||||||
|
{manga?.tags.map(tag => <Chip key={tag} size={"sm"} sx={{backgroundColor: theme.palette.primary.plainColor}}>{tag}</Chip>)}
|
||||||
|
{manga?.authors.map(author => <Chip key={author.key} size={'sm'} sx={{backgroundColor: theme.palette.success.plainColor}}>{author.name}</Chip> )}
|
||||||
|
{manga?.links.map(link => <Chip key={link.provider} size={"sm"} sx={{backgroundColor: theme.palette.neutral.plainColor}}><a href={link.url}>{link.provider}</a></Chip>)}
|
||||||
|
</Stack>
|
||||||
|
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: theme.palette.text.primary, overflowY: "auto"}}/>
|
||||||
|
</Stack>
|
||||||
|
<Stack sx={{width: '100%'}} flexWrap={'nowrap'} gap={1}>
|
||||||
|
{props.actions}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MangaDetailProps {
|
||||||
|
manga?: Manga;
|
||||||
|
mangaKey?: string;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<boolean>;
|
||||||
|
actions?: ReactNode[];
|
||||||
|
}
|
@@ -16,60 +16,64 @@ import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx'
|
|||||||
import TInput from './Components/Inputs/TInput.tsx'
|
import TInput from './Components/Inputs/TInput.tsx'
|
||||||
import { ApiContext } from './contexts/ApiContext.tsx'
|
import { ApiContext } from './contexts/ApiContext.tsx'
|
||||||
import { MangaCardList } from './Components/Mangas/MangaList.tsx'
|
import { MangaCardList } from './Components/Mangas/MangaList.tsx'
|
||||||
import { MangaConnector, MinimalManga } from './api/data-contracts.ts'
|
import {MangaConnector, MinimalManga} from './api/data-contracts.ts'
|
||||||
|
import MangaDetail from "./MangaDetail.tsx";
|
||||||
|
|
||||||
export default function Search({
|
export function Search(props: SearchModalProps): ReactNode {
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
open: boolean
|
|
||||||
setOpen: Dispatch<boolean>
|
|
||||||
}): ReactNode {
|
|
||||||
const Api = useContext(ApiContext)
|
const Api = useContext(ApiContext)
|
||||||
const MangaConnectors = useContext(MangaConnectorContext)
|
const MangaConnectors = useContext(MangaConnectorContext)
|
||||||
|
|
||||||
const [selectedConnector, setSelectedConnector] = useState<MangaConnector>()
|
|
||||||
|
|
||||||
const [searchResults, setSearchResults] = useState<MinimalManga[]>([])
|
|
||||||
|
|
||||||
const startSearch = (
|
|
||||||
value: string | number | readonly string[] | undefined
|
|
||||||
): Promise<void> => {
|
|
||||||
if (typeof value != 'string') return Promise.reject()
|
|
||||||
setSearchResults([])
|
|
||||||
if (isUrl(value)) {
|
|
||||||
return Api.searchUrlCreate(value)
|
|
||||||
.then((result) => {
|
|
||||||
if (result.ok) {
|
|
||||||
setSearchResults([result.data])
|
|
||||||
return Promise.resolve()
|
|
||||||
} else return Promise.reject()
|
|
||||||
})
|
|
||||||
.catch(Promise.reject)
|
|
||||||
} else {
|
|
||||||
if (!selectedConnector) return Promise.reject()
|
|
||||||
return Api.searchDetail(selectedConnector?.key, value)
|
|
||||||
.then((result) => {
|
|
||||||
if (result.ok) {
|
|
||||||
setSearchResults(result.data)
|
|
||||||
return Promise.resolve()
|
|
||||||
} else return Promise.reject()
|
|
||||||
})
|
|
||||||
.catch(Promise.reject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open){
|
if (props.open) {
|
||||||
setSelectedConnector(undefined);
|
setSelectedConnector(undefined);
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
const [selectedConnector, setSelectedConnector] = useState<MangaConnector>()
|
||||||
|
const [searchResults, setSearchResults] = useState<MinimalManga[]>([])
|
||||||
|
|
||||||
|
const startSearch = async (
|
||||||
|
value: string | number | readonly string[] | undefined
|
||||||
|
): Promise<void> => {
|
||||||
|
if (typeof value != 'string') return Promise.reject()
|
||||||
|
setSearchResults([])
|
||||||
|
if (isUrl(value)) {
|
||||||
|
try {
|
||||||
|
let result = await Api.searchUrlCreate(value);
|
||||||
|
if (result.ok) {
|
||||||
|
setSearchResults([result.data])
|
||||||
|
return Promise.resolve()
|
||||||
|
} else return Promise.reject()
|
||||||
|
} catch (reason) {
|
||||||
|
return await Promise.reject(reason);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!selectedConnector) return Promise.reject()
|
||||||
|
try {
|
||||||
|
let result2 = await Api.searchDetail(selectedConnector?.key, value);
|
||||||
|
if (result2.ok) {
|
||||||
|
setSearchResults(result2.data)
|
||||||
|
return Promise.resolve()
|
||||||
|
} else return Promise.reject()
|
||||||
|
} catch (reason1) {
|
||||||
|
return await Promise.reject(reason1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [selectedManga, setSelectedManga] = useState<MinimalManga | undefined>(undefined);
|
||||||
|
const [mangaDetailOpen, setMangaDetailOpen] = useState(false);
|
||||||
|
|
||||||
|
function openMangaDetail(manga: MinimalManga) {
|
||||||
|
setSelectedManga(manga);
|
||||||
|
setMangaDetailOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={() => setOpen(false)}>
|
<Modal open={props.open} onClose={() => props.setOpen(false)}>
|
||||||
<ModalDialog sx={{ width: '90vw' }}>
|
<ModalDialog sx={{width: '90vw'}}>
|
||||||
<ModalClose />
|
<ModalClose/>
|
||||||
<Stepper>
|
<Stepper>
|
||||||
<Step
|
<Step
|
||||||
orientation={'vertical'}
|
orientation={'vertical'}
|
||||||
@@ -81,6 +85,7 @@ export default function Search({
|
|||||||
<List>
|
<List>
|
||||||
{MangaConnectors.map((c) => (
|
{MangaConnectors.map((c) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
key={c.key}
|
||||||
onClick={() => setSelectedConnector(c)}
|
onClick={() => setSelectedConnector(c)}
|
||||||
>
|
>
|
||||||
<ListItemDecorator>
|
<ListItemDecorator>
|
||||||
@@ -91,7 +96,7 @@ export default function Search({
|
|||||||
<Typography
|
<Typography
|
||||||
sx={
|
sx={
|
||||||
c.key == selectedConnector?.key
|
c.key == selectedConnector?.key
|
||||||
? { fontWeight: 'bold' }
|
? {fontWeight: 'bold'}
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -114,12 +119,18 @@ export default function Search({
|
|||||||
/>
|
/>
|
||||||
</Step>
|
</Step>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
<MangaCardList manga={searchResults} />
|
<MangaCardList manga={searchResults} mangaOnClick={openMangaDetail}/>
|
||||||
|
<MangaDetail mangaKey={selectedManga?.key} open={mangaDetailOpen} setOpen={setMangaDetailOpen} />
|
||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchModalProps {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
function isUrl(str: string): boolean {
|
function isUrl(str: string): boolean {
|
||||||
try {
|
try {
|
||||||
new URL(str)
|
new URL(str)
|
||||||
@@ -127,4 +138,4 @@ function isUrl(str: string): boolean {
|
|||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user