This commit is contained in:
monoid 2021-01-08 10:46:19 +09:00
parent 541de21e66
commit 5002bc22f9
9 changed files with 271 additions and 190 deletions

View File

@ -1,27 +1,24 @@
import React from 'react'; import React, { useRef, useState } from 'react';
import ReactDom from 'react-dom'; import ReactDom from 'react-dom';
import {BrowserRouter, Route, Switch as RouterSwitch} from 'react-router-dom'; import {BrowserRouter, Route, Switch as RouterSwitch} from 'react-router-dom';
import {Headline} from './page/headline'; import { Gallery, ContentAbout} from './page/mod';
import {Gallery} from './page/gallery';
import {ContentAbout} from './page/contentinfo';
import './css/style.css'; import './css/style.css';
const FooProfile = ()=><div>test profile</div>; const FooProfile = ()=><div>test profile</div>;
const App = ()=> ( const App = () => {
<BrowserRouter> return (<BrowserRouter>
<Headline>
<RouterSwitch> <RouterSwitch>
<Route path="/" exact component={Gallery}></Route> <Route path="/" exact render={()=><Gallery />}></Route>
<Route path="/doc" component={ContentAbout}></Route> <Route path="/search" render={()=><Gallery />}></Route>
<Route path="/doc" render={(prop)=><ContentAbout {...prop}/>}></Route>
<Route path="/profile" component={FooProfile}></Route> <Route path="/profile" component={FooProfile}></Route>
<Route> <Route>
<div>404 Not Found</div> <div>404 Not Found</div>
</Route> </Route>
</RouterSwitch> </RouterSwitch>
</Headline> </BrowserRouter>);
</BrowserRouter> };
);
ReactDom.render( ReactDom.render(
<App/>, <App/>,

View File

@ -34,34 +34,93 @@ const useStyles = makeStyles((theme: Theme) => ({
title: { title: {
marginLeft: theme.spacing(1), marginLeft: theme.spacing(1),
}, },
InfoContainer: { infoContainer: {
padding: theme.spacing(2),
},
subinfoContainer: {
display: 'grid', display: 'grid',
gridTemplateColumns: '100px auto', gridTemplateColumns: '100px auto',
overflowY: 'hidden', overflowY: 'hidden',
},
short_subinfoContainer:{
[theme.breakpoints.up("xs")]:{
display:'none',
} }
},
short_root:{
overflowY:'hidden',
display:'flex',
flexDirection: 'column',
[theme.breakpoints.up("sm")]:{
height:200,
flexDirection: 'row',
},
},
short_thumbnail_anchor:{
background: '#272733',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
[theme.breakpoints.up("sm")]:{
width: theme.spacing(25),
height: theme.spacing(25),
flexShrink: 0,
}
},
short_thumbnail_content: {
maxWidth: '100%',
maxHeight: '100%',
},
})) }))
export const ContentInfo = (props: { content: Content, children?: React.ReactNode }) => { export const ContentInfo = (props: {
content: Content, children?: React.ReactNode, classes?: {
root?: string,
thumbnail_anchor?: string,
thumbnail_content?: string,
tag_list?: string,
title?: string,
infoContainer?: string,
subinfoContainer?: string
},
gallery?:string,
short?:boolean
}) => {
const classes = useStyles(); const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
const content = props?.content; const content = props.content;
const propclasses = props.classes || {};
const rootName = props.short ? classes.short_root : classes.root;
const thumbnail_anchor = props.short ? classes.short_thumbnail_anchor : "";
const thumbnail_content = props.short ? classes.short_thumbnail_content :
classes.thumbnail_content;
const subinfoContainer = props.short ? classes.short_subinfoContainer :
classes.subinfoContainer;
let allTag = content.tags; let allTag = content.tags;
const artists = allTag.filter(x => x.startsWith("artist:")).map(x => x.substr(7)); const artists = allTag.filter(x => x.startsWith("artist:")).map(x => x.substr(7));
allTag = allTag.filter(x => !x.startsWith("artist:")); allTag = allTag.filter(x => !x.startsWith("artist:"));
return (<Paper className={classes.root}> return (<Paper className={propclasses.root || rootName} elevation={4}>
<Link component={RouterLink} to={makeContentReaderUrl(content.id)}> <Link className={propclasses.thumbnail_anchor || thumbnail_anchor} component={RouterLink} to={{
<ThumbnailContainer content={content} className={classes.thumbnail_content}></ThumbnailContainer> pathname:makeContentReaderUrl(content.id),
state:props.gallery
}}>
<ThumbnailContainer content={content}
className={propclasses.thumbnail_content || thumbnail_content}/>
</Link> </Link>
<Box style={{ padding: theme.spacing(1) }}> <Box className={propclasses.infoContainer || classes.infoContainer}>
<Link variant='h5' color='inherit' component={RouterLink} to={makeContentReaderUrl(content.id)} <Link variant='h5' color='inherit' component={RouterLink} to={{
className={classes.title}> pathname: props.gallery === undefined ? makeContentReaderUrl(content.id) : makeContentInfoUrl(content.id),
state:props.gallery
}}
className={propclasses.title || classes.title}>
{content.title} {content.title}
</Link> </Link>
<Box className={classes.InfoContainer}> <Box className={propclasses.subinfoContainer || subinfoContainer}>
<Typography variant='subtitle1'>Artist</Typography> <Typography variant='subtitle1'>Artist</Typography>
<Box style={{ alignSelf: "center" }}>{artists.join(", ")}</Box> <Box style={{ alignSelf: "center" }}>{artists.join(", ")}</Box>
<Typography variant='subtitle1'>Tags</Typography> <Typography variant='subtitle1'>Tags</Typography>
<Box className={classes.tag_list}> <Box className={propclasses.tag_list || classes.tag_list}>
{allTag.map(x => { {allTag.map(x => {
return (<TagChip key={x} label={x} clickable={true} tagname={x} size="small"></TagChip>); return (<TagChip key={x} label={x} clickable={true} tagname={x} size="small"></TagChip>);
})} })}

View File

@ -0,0 +1,71 @@
import { Box, Paper, Link, useMediaQuery, Portal, List, ListItem, ListItemIcon, Tooltip, ListItemText } from '@material-ui/core';
import {useTheme, makeStyles, Theme} from '@material-ui/core/styles';
import React, { useEffect, useState } from 'react';
import ContentAccessor,{QueryListOption, Content} from '../accessor/contents';
import {Link as RouterLink} from 'react-router-dom';
import { LoadingCircle, ContentInfo, NavList, NavItem } from './mod';
import {toQueryString} from '../accessor/util';
const useStyles = makeStyles((theme:Theme)=>({
root:{
display:"grid",
gridGap: theme.spacing(4),
},
anchor_thumbnail:{
background: '#272733',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
[theme.breakpoints.up("sm")]:{
width: theme.spacing(25),
height: theme.spacing(25),
flexShrink: 0,
}
},
contentPaper:{
overflowY:'hidden',
display:'flex',
flexDirection: 'column',
[theme.breakpoints.up("sm")]:{
height:200,
flexDirection: 'row',
},
},
content_thumnail: {
maxWidth: '100%',
maxHeight: '100%',
},
}));
export type GalleryProp = {
option?:QueryListOption;
};
type GalleryState = {
content:Content[]|undefined;
}
export const GalleryInfo = (props: GalleryProp)=>{
const [state,setState]= useState<GalleryState>({content:undefined});
useEffect(()=>{
(async ()=>{
const c = await ContentAccessor.findList(props.option);
setState({content:c});
})()
},[props.option]);
const classes = useStyles();
const queryString = toQueryString(props.option||{});
if(state.content === undefined){
return (<LoadingCircle/>);
}
else{
return (<Box className={classes.root}>{
state.content.map(x=>{
return (<ContentInfo content={x} key={x.id}
gallery={`/search?${queryString}`} short/>);
})
}
</Box>);
}
}

View File

@ -1,13 +1,13 @@
import ReactDom from 'react-dom'; import ReactDom from 'react-dom';
import React, { useState } from 'react'; import React, { ReactNode, useState } from 'react';
import { import {
Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer, Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer,
AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem, AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem,
Hidden, Tooltip Hidden, Tooltip, Link
} from '@material-ui/core'; } from '@material-ui/core';
import { makeStyles, Theme, useTheme, fade } from '@material-ui/core/styles'; import { makeStyles, Theme, useTheme, fade } from '@material-ui/core/styles';
import { ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon, ArrowBack as ArrowBackIcon, AccountCircle } from '@material-ui/icons'; import { ChevronLeft, ChevronRight, Menu as MenuIcon, Search as SearchIcon, AccountCircle } from '@material-ui/icons';
import {Link} from 'react-router-dom'; import { Link as RouterLink, useRouteMatch } from 'react-router-dom';
const drawerWidth = 240; const drawerWidth = 240;
@ -104,7 +104,12 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
})); }));
export const Headline = (prop: { children?: React.ReactNode, navListItem?: React.ReactNode, isLogin?:boolean}) => { export const Headline = (prop: {
children?: React.ReactNode,
navListItem?: React.ReactNode,
isLogin?: boolean,
menu: React.ReactNode
}) => {
const [v, setv] = useState(false); const [v, setv] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const classes = useStyles(); const classes = useStyles();
@ -124,7 +129,7 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
transformOrigin={{ horizontal: 'right', vertical: "top" }} transformOrigin={{ horizontal: 'right', vertical: "top" }}
onClose={handleProfileMenuClose} onClose={handleProfileMenuClose}
> >
<MenuItem component={Link} to='/profile'>Profile</MenuItem> <MenuItem component={RouterLink} to='/profile'>Profile</MenuItem>
<MenuItem>Logout</MenuItem> <MenuItem>Logout</MenuItem>
</Menu>); </Menu>);
const drawer_contents = (<> const drawer_contents = (<>
@ -134,17 +139,7 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
</IconButton> </IconButton>
</div> </div>
<Divider /> <Divider />
<List> {prop.menu}
<ListItem button key="Back">
<ListItemIcon>
<Tooltip title="back" placement="bottom">
<ArrowBackIcon></ArrowBackIcon>
</Tooltip>
</ListItemIcon>
<ListItemText primary="Back"></ListItemText>
</ListItem>
{prop.navListItem}
</List>
</>); </>);
return (<div className={classes.root}> return (<div className={classes.root}>
<CssBaseline /> <CssBaseline />
@ -157,9 +152,9 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
className={classes.menuButton}> className={classes.menuButton}>
<MenuIcon></MenuIcon> <MenuIcon></MenuIcon>
</IconButton> </IconButton>
<Typography variant="h5" noWrap className={classes.title}> <Link variant="h5" noWrap className={classes.title} color="inherit" component={RouterLink} to="/">
Ionian Ionian
</Typography> </Link>
<div className={classes.grow}></div> <div className={classes.grow}></div>
<div className={classes.search}> <div className={classes.search}>
<div className={classes.searchIcon}> <div className={classes.searchIcon}>

View File

@ -1,3 +1,6 @@
export * from './contentinfo'; export * from './contentinfo';
export * from './loading'; export * from './loading';
export * from './tagchip'; export * from './tagchip';
export * from './navlist';
export * from './galleryinfo';
export * from './headline';

View File

@ -0,0 +1,21 @@
import {List, ListItem, ListItemIcon, Tooltip, ListItemText} from '@material-ui/core';
import React from 'react';
import {Link as RouterLink} from 'react-router-dom';
import {LocationDescriptorObject} from 'history';
export const NavItem = (props:{name:string,to:string, icon:React.ReactElement<any,any>})=>{
return (<ListItem button key={props.name} component={RouterLink} to={props.to}>
<ListItemIcon>
<Tooltip title={props.name.toLocaleLowerCase()} placement="bottom">
{props.icon}
</Tooltip>
</ListItemIcon>
<ListItemText primary={props.name}></ListItemText>
</ListItem>);
}
export const NavList = (props: {children?:React.ReactNode})=>{
return (<List>
{props.children}
</List>);
}

View File

@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink } from 'react-router-dom'; import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink, useParams, useLocation } from 'react-router-dom';
import ContentAccessor, { Content } from '../accessor/contents'; import ContentAccessor, { Content } from '../accessor/contents';
import { LoadingCircle } from '../component/loading'; import { LoadingCircle } from '../component/loading';
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core'; import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
import {ArrowBack as ArrowBackIcon } from '@material-ui/icons';
import { MangaReader } from './reader/manga'; import { MangaReader } from './reader/manga';
import { ContentInfo } from '../component/mod'; import { ContentInfo, Headline, NavItem, NavList } from '../component/mod';
export const makeContentInfoUrl = (id: number) => `/doc/${id}`; export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
export const makeMangaReaderUrl = (id: number) => `/doc/${id}/reader`; export const makeMangaReaderUrl = (id: number) => `/doc/${id}/reader`;
@ -15,39 +16,65 @@ type ContentState = {
} }
export const ContentAbout = (prop: { match: MatchType }) => { export const ContentAbout = (prop: { match: MatchType }) => {
const match = useRouteMatch("/doc/:id"); const match = useRouteMatch<{id:string}>("/doc/:id");
if (match == null) { if (match == null) {
throw new Error("unreachable"); throw new Error("unreachable");
} }
const m = /\/doc\/(\d+)/.exec(match.url); const id = Number.parseInt(match.params['id']);
const id = m !== null ? Number.parseInt(m[1]) : NaN;
const [info, setInfo] = useState<ContentState>({ content: undefined, notfound:false }); const [info, setInfo] = useState<ContentState>({ content: undefined, notfound:false });
const location = useLocation();
console.log("state : "+location.state);
const menu_list = (
<NavList>
<NavItem name="Back" to="/" icon={<ArrowBackIcon/>}></NavItem>
</NavList>
);
useEffect(() => { useEffect(() => {
(async () => { (async () => {
console.log("mount content about");
if (!isNaN(id)) { if (!isNaN(id)) {
const c = await ContentAccessor.findById(id); const c = await ContentAccessor.findById(id);
setInfo({ content: c, notfound: c === undefined }); setInfo({ content: c, notfound: c === undefined });
} }
})() })()
return ()=>{console.log("unmount content about")}
}, []); }, []);
if (isNaN(id)) { if (isNaN(id)) {
return (<Typography variant='h2'>Oops. Invalid ID</Typography>) return (
<Headline menu={menu_list}>
<Typography variant='h2'>Oops. Invalid ID</Typography>
</Headline>
);
} }
else if(info.notfound){ else if(info.notfound){
return (<Typography variant='h2'>Content has been removed.</Typography>) return (
<Headline menu={menu_list}>
<Typography variant='h2'>Content has been removed.</Typography>
</Headline>
)
} }
else if (info.content === undefined) { else if (info.content === undefined) {
return (<LoadingCircle />); return (<Headline menu={menu_list}>
<LoadingCircle />
</Headline>
);
} }
else return (<Switch> else return (<Switch>
<Route exact path={`${prop.match.path}/:id`}> <Route exact path={`${prop.match.path}/:id`}>
<Headline menu={menu_list}>
<ContentInfo content={info.content}></ContentInfo> <ContentInfo content={info.content}></ContentInfo>
</Headline>
</Route> </Route>
<Route exact path={`${prop.match.path}/:id/reader`}> <Route exact path={`${prop.match.path}/:id/reader`}>
<Headline menu={menu_list}>
<MangaReader content={info.content}></MangaReader> <MangaReader content={info.content}></MangaReader>
</Headline>
</Route> </Route>
<Route> <Route>
<Headline menu={menu_list}>
<div>404 Not Found invalid url : {prop.match.path}</div> <div>404 Not Found invalid url : {prop.match.path}</div>
</Headline>
</Route> </Route>
</Switch>); </Switch>);
} }

View File

@ -1,108 +1,14 @@
import { Box, Paper, Link, useMediaQuery } from '@material-ui/core'; import React from 'react';
import React, { useEffect, useState } from 'react'; import { NavList, NavItem, Headline } from '../component/mod';
import ContentAccessor,{makeThumbnailUrl, QueryListOption, Content} from '../accessor/contents'; import {ArrowBack as ArrowBackIcon} from '@material-ui/icons';
import {useTheme, makeStyles, Theme} from '@material-ui/core/styles'; import {GalleryInfo} from '../component/mod';
import {Link as RouterLink} from 'react-router-dom';
import {makeContentInfoUrl} from './contentinfo';
import { LoadingCircle } from '../component/loading';
import {ThumbnailContainer} from './reader/reader';
import {TagChip} from '../component/tagchip';
const useStyles = makeStyles((theme:Theme)=>({ export const Gallery = ()=>{
root:{ const menu_list = (<NavList>
display:"grid", <NavItem name="Back" to="" icon={<ArrowBackIcon></ArrowBackIcon>}></NavItem>
gridGap: theme.spacing(4), </NavList>);
},
anchor_thumbnail:{
background: '#272733',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
[theme.breakpoints.up("sm")]:{
width: theme.spacing(25),
height: theme.spacing(25),
}
},
contentPaper:{
display:'flex',
flexDirection: 'column',
[theme.breakpoints.up("sm")]:{
height:200,
flexDirection: 'row',
},
},
content_thumnail: {
maxWidth: '100%',
maxHeight: '100%',
},
content_info:{
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
width: '100%',
display: 'flex',
flexDirection: 'column',
},
content_info_title:{
marginLeft:theme.spacing(2),
},
tag_list:{
display:'none',
[theme.breakpoints.up("sm")]:{
display: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
overflowY: 'hidden',
'& > *': {
margin: theme.spacing(0.5),
},
},
},
}));
export type GalleryProp = { return (<Headline menu={menu_list}>
option?:QueryListOption; <GalleryInfo></GalleryInfo>
}; </Headline>)
type GalleryState = {
content:Content[]|undefined;
}
export const Gallery = (props: GalleryProp)=>{
const [state,setState]= useState<GalleryState>({content:undefined});
useEffect(()=>{
(async ()=>{
const c = await ContentAccessor.findList(props.option);
setState({content:c});
})()
},[props.option]);
const num = 1;
const classes = useStyles();
const theme = useTheme();
const isMobile = !useMediaQuery(theme.breakpoints.up("sm"));
if(state.content === undefined){
return (<LoadingCircle/>);
}
else{
return (<div className={classes.root}>{
state.content.map(x=>{
const thumbnail_url = makeThumbnailUrl(x);
return (<Paper key={x.id} elevation={4} className={classes.contentPaper}>
<Link className={classes.anchor_thumbnail}
component={RouterLink} to={makeContentInfoUrl(x.id)}>
<ThumbnailContainer content={x} className={classes.content_thumnail}></ThumbnailContainer>
</Link>
<Box className={classes.content_info}>
<Link component={RouterLink} to={makeContentInfoUrl(x.id)} variant="h5" color="inherit"
className={classes.content_info_title}>
{x.title}
</Link>
<Box className={classes.tag_list}>
{x.tags.map(x=>{
return (<TagChip key={x} label={x} clickable={true} tagname={x} size="small"></TagChip>);
})}
</Box>
</Box>
</Paper>);
})
}
</div>);
}
} }

2
src/client/page/mod.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './contentinfo';
export * from './gallery';