move file

This commit is contained in:
monoid 2021-01-06 20:16:27 +09:00
parent 790df54525
commit 4e03649880
13 changed files with 378 additions and 193 deletions

View File

@ -2,7 +2,8 @@ import {Content, ContentAccessor, ContentContent, QueryListOption} from "../../m
import {toQueryString} from './util'; import {toQueryString} from './util';
const baseurl = "/content"; const baseurl = "/content";
export class ClientContentAccessesor implements ContentAccessor{ export * from "../../model/contents";
export class ClientContentAccessor implements ContentAccessor{
async findList(option?: QueryListOption | undefined): Promise<Content[]>{ async findList(option?: QueryListOption | undefined): Promise<Content[]>{
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`); let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
//if(res.status !== 200); //if(res.status !== 200);
@ -65,3 +66,9 @@ export class ClientContentAccessesor implements ContentAccessor{
return ret; return ret;
} }
} }
export const CContentAccessor = new ClientContentAccessor;
export const makeThumbnailUrl = (x: Content)=>{
return `/content/${x.id}/${x.content_type}/thumbnail`;
}
export default CContentAccessor;

View File

@ -1,11 +1,11 @@
import React from 'react'; import React 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 './headline'; import {Headline} from './page/headline';
import {Gallery} from './gallery'; import {Gallery} from './page/gallery';
import {ContentInfo} from './contentinfo'; 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 = ()=> (
@ -13,10 +13,8 @@ const App = ()=> (
<Headline> <Headline>
<RouterSwitch> <RouterSwitch>
<Route path="/" exact component={Gallery}></Route> <Route path="/" exact component={Gallery}></Route>
<Route path="/content" component={ContentInfo}> <Route path="/doc" component={ContentAbout}></Route>
</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>

View File

@ -0,0 +1,8 @@
import React from 'react';
import {Box, CircularProgress} from '@material-ui/core';
export const LoadingCircle = ()=>{
return (<div><Box>
<CircularProgress title="loading"/>
</Box></div>);
}

View File

@ -0,0 +1,64 @@
import React from 'react';
import {ChipProps} from '@material-ui/core/Chip';
import { Chip, colors } from '@material-ui/core';
import {useTheme, makeStyles, Theme, emphasize, fade} from '@material-ui/core/styles';
type TagChipStyleProp = {
color: string
}
const useTagStyles = makeStyles((theme:Theme)=>({
root:(props:TagChipStyleProp)=>({
color: theme.palette.getContrastText(props.color),
backgroundColor: props.color,
}),
clickable:(props:TagChipStyleProp)=>({
'&:hover, &:focus':{
backgroundColor:emphasize(props.color,0.08)
}
}),
deletable: {
'&:focus': {
backgroundColor: (props:TagChipStyleProp)=>emphasize(props.color, 0.2),
}
},
outlined:{
color: (props:TagChipStyleProp)=>props.color,
border: (props:TagChipStyleProp)=> `1px solid ${props.color}`,
'$clickable&:hover, $clickable&:focus, $deletable&:focus': {
backgroundColor:(props:TagChipStyleProp)=>fade(props.color,theme.palette.action.hoverOpacity),
},
},
icon:{
color:"inherit",
},
deleteIcon:{
color:(props:TagChipStyleProp)=>fade(theme.palette.getContrastText(props.color),0.7),
"&:hover, &:active":{
color:(props:TagChipStyleProp)=>theme.palette.getContrastText(props.color),
}
}
}));
const {blue, pink} = colors;
const getTagColorName = (tagname :string):string=>{
if(tagname.startsWith("female")){
return pink['700'];
}
else if(tagname.startsWith("male")){
return blue[600];
}
else return "default";
}
export const ColorChip = (props:Omit<ChipProps,"color"> & {color: string})=>{
const {color,...rest} = props;
const classes = useTagStyles({color : color !== "default" ? color : "#000"});
return <Chip color="default" classes={{
...(color !== "default" ? classes : {})
}} {...rest}></Chip>;
}
export const TagChip = (props:Omit<ChipProps,"color"> & {tagname: string})=>{
const {tagname,...rest} = props;
return <ColorChip color={getTagColorName(tagname)} {...rest}></ColorChip>;
}

View File

@ -1,9 +0,0 @@
import React from 'react';
import {useHistory, useRouteMatch} from 'react-router-dom';
export const makeContentInfoUrl = (id:number)=>`/content/${id}`;
export const ContentInfo = (props?:{children?:React.ReactNode})=>{
const match = useRouteMatch("/content/:id");
return(<div>{match?.url}</div>)
}

View File

@ -1,157 +0,0 @@
import { Box, CircularProgress, Typography, Paper, Chip, withStyles, colors, Link } from '@material-ui/core';
import {ChipProps} from '@material-ui/core/Chip';
import React, { useEffect, useState } from 'react';
import {QueryListOption,Content} from '../../model/contents';
import {ClientContentAccessesor} from '../accessor/contents';
import {useTheme, makeStyles, Theme, emphasize, fade} from '@material-ui/core/styles';
import {blue , pink} from '@material-ui/core/colors';
import {Link as RouterLink} from 'react-router-dom';
import {makeContentInfoUrl} from './contentinfo';
type TagChipStyleProp = {
color: string
}
const useTagStyles = makeStyles((theme:Theme)=>({
root:(props:TagChipStyleProp)=>({
color: theme.palette.getContrastText(props.color),
backgroundColor: props.color,
}),
clickable:(props:TagChipStyleProp)=>({
'&:hover, &:focus':{
backgroundColor:emphasize(props.color,0.08)
}
}),
deletable: {
'&:focus': {
backgroundColor: (props:TagChipStyleProp)=>emphasize(props.color, 0.2),
}
},
outlined:{
color: (props:TagChipStyleProp)=>props.color,
border: (props:TagChipStyleProp)=> `1px solid ${props.color}`,
'$clickable&:hover, $clickable&:focus, $deletable&:focus': {
backgroundColor:(props:TagChipStyleProp)=>fade(props.color,theme.palette.action.hoverOpacity),
},
},
icon:{
color:"inherit",
},
deleteIcon:{
color:(props:TagChipStyleProp)=>fade(theme.palette.getContrastText(props.color),0.7),
"&:hover, &:active":{
color:(props:TagChipStyleProp)=>theme.palette.getContrastText(props.color),
}
}
}));
const getTagColorName = (tagname :string):string=>{
if(tagname.startsWith("female")){
return pink['700'];
}
else if(tagname.startsWith("male")){
return blue[600];
}
else return "default";
}
const TagChip = (props:Omit<ChipProps,"color"> & {color: string})=>{
const {color,...rest} = props;
const classes = useTagStyles({color : color !== "default" ? color : "#000"});
return <Chip color="default" classes={{
...(color !== "default" ? classes : {})
}} {...rest}></Chip>;
}
const useStyles = makeStyles((theme:Theme)=>({
root:{
display:"grid",
gridGap: theme.spacing(4),
},
table_thumbnail:{
width: theme.spacing(25),
height: theme.spacing(25),
background: '#272733',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
contentPaper:{
display:'flex',
height:200,
},
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: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
overflowY: 'hidden',
'& > *': {
margin: theme.spacing(0.5),
},
},
}));
export type GalleryProp = {
option?:QueryListOption;
};
type GalleryState = {
content:Content[]|undefined;
}
const content_accessor = new ClientContentAccessesor;
export const Gallery = (props: GalleryProp)=>{
const [state,setState]= useState<GalleryState>({content:undefined});
useEffect(()=>{
(async ()=>{
const c = await content_accessor.findList(props.option);
setState({content:c});
})()
},[props.option]);
const num = 1;
const classes = useStyles();
const theme = useTheme();
if(state.content === undefined){
return (<div><Box>
<CircularProgress title="loading"/>
</Box></div>);
}
else{
return (<div className={classes.root}>{
state.content.map(x=>{
const thumbnail_url = `/content/${x.id}/${x.content_type}/thumbnail`;
return (<Paper key={x.id} elevation={4} className={classes.contentPaper}>
<Link className={classes.table_thumbnail} component={RouterLink} to={makeContentInfoUrl(x.id)}>{
x.content_type === "manga" ? <img className={classes.content_thumnail} src={thumbnail_url}></img> :
<video className={classes.content_thumnail} src={thumbnail_url}></video>
}</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=>{
const tagcolor = getTagColorName(x);
return (<TagChip key={x} label={x} clickable={true} color={tagcolor} size="small"></TagChip>);
})}
</Box>
</Box>
</Paper>);
})
}
</div>);
}
}

View File

@ -0,0 +1,115 @@
import React, { useState, useEffect } from 'react';
import { Redirect, Route, Switch, useHistory, useRouteMatch, match as MatchType, Link as RouterLink } from 'react-router-dom';
import ContentAccessor, { Content } from '../accessor/contents';
import { LoadingCircle } from '../component/loading';
import { Link, Paper, makeStyles, Theme, Box, useTheme, Typography } from '@material-ui/core';
import { ThumbnailContainer } from '../presenter/presenter';
import { TagChip } from '../component/tagchip';
import { MangaReader } from '../presenter/manga';
export const makeContentInfoUrl = (id: number) => `/doc/${id}`;
export const makeMangaReaderUrl = (id: number) => `/doc/${id}/reader`;
type ContentInfoState = {
content: Content | undefined,
notfound: boolean,
}
export const ContentAbout = (prop: { match: MatchType }) => {
const match = useRouteMatch("/doc/:id");
if (match == null) {
throw new Error("unreachable");
}
const m = /\/doc\/(\d+)/.exec(match.url);
const id = m !== null ? Number.parseInt(m[1]) : NaN;
const [info, setInfo] = useState<ContentInfoState>({ content: undefined, notfound:false });
useEffect(() => {
(async () => {
if (!isNaN(id)) {
const c = await ContentAccessor.findById(id);
setInfo({ content: c, notfound: c === undefined });
}
})()
}, []);
if (isNaN(id)) {
return (<Typography variant='h2'>Oops. Invalid ID</Typography>)
}
else if(info.notfound){
return (<Typography variant='h2'>Content has been removed.</Typography>)
}
else if (info.content === undefined) {
return (<LoadingCircle />);
}
else return (<Switch>
<Route exact path={`${prop.match.path}/:id`}>
<ContentInfo content={info.content}></ContentInfo>
</Route>
<Route exact path={`${prop.match.path}/:id/reader`}>
<MangaReader content={info.content}></MangaReader>
</Route>
<Route>
<div>404 Not Found invalid url : {prop.match.path}</div>
</Route>
</Switch>);
}
const useStyles = makeStyles((theme: Theme) => ({
root: {
display: "flex",
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
alignItems: "center",
}
},
thumbnail_content: {
maxWidth: '300px',
maxHeight: '300px'
},
tag_list: {
display: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
overflowY: 'hidden',
'& > *': {
margin: theme.spacing(0.5),
},
},
title: {
marginLeft: theme.spacing(1),
},
InfoContainer: {
display: 'grid',
gridTemplateColumns: '100px auto',
overflowY: 'hidden',
}
}))
export const ContentInfo = (props: { content: Content, children?: React.ReactNode }) => {
const classes = useStyles();
const theme = useTheme();
const content = props?.content;
let allTag = content.tags;
const artists = allTag.filter(x => x.startsWith("artist:")).map(x => x.substr(7));
allTag = allTag.filter(x => !x.startsWith("artist:"));
return (<Paper className={classes.root}>
<Link component={RouterLink} to={makeMangaReaderUrl(content.id)}>
<ThumbnailContainer content={content} className={classes.thumbnail_content}></ThumbnailContainer>
</Link>
<Box style={{ padding: theme.spacing(1) }}>
<Link variant='h5' color='inherit' component={RouterLink} to={makeMangaReaderUrl(content.id)}
className={classes.title}>
{content.title}
</Link>
<Box className={classes.InfoContainer}>
<Typography variant='subtitle1'>Artist</Typography>
<Box>{artists.join(", ")}</Box>
<Typography variant='subtitle1'>Tags</Typography>
<Box className={classes.tag_list}>
{allTag.map(x => {
return (<TagChip key={x} label={x} clickable={true} tagname={x} size="small"></TagChip>);
})}
</Box>
</Box>
</Box>
</Paper>);
}

108
src/client/page/gallery.tsx Normal file
View File

@ -0,0 +1,108 @@
import { Box, Paper, Link, useMediaQuery } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import ContentAccessor,{makeThumbnailUrl, QueryListOption, Content} from '../accessor/contents';
import {useTheme, makeStyles, Theme} from '@material-ui/core/styles';
import {Link as RouterLink} from 'react-router-dom';
import {makeContentInfoUrl} from './contentinfo';
import { LoadingCircle } from '../component/loading';
import {ThumbnailContainer} from '../presenter/presenter';
import {TagChip} from '../component/tagchip';
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),
}
},
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 = {
option?:QueryListOption;
};
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>);
}
}

View File

@ -3,7 +3,7 @@ import React, { 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, useMediaQuery, Tooltip Hidden, Tooltip
} 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, ArrowBack as ArrowBackIcon, AccountCircle } from '@material-ui/icons';
@ -45,13 +45,11 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
}, },
toolbar: { toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
...theme.mixins.toolbar, ...theme.mixins.toolbar,
}, },
content: { content: {
display:'flex',
flexFlow: 'column',
flexGrow: 1, flexGrow: 1,
padding: theme.spacing(3), padding: theme.spacing(3),
}, },
@ -106,7 +104,7 @@ const useStyles = makeStyles((theme: Theme) => ({
}, },
})); }));
export const Headline = (prop: { children?: React.ReactNode, navListItem?: React.ReactNode}) => { export const Headline = (prop: { children?: React.ReactNode, navListItem?: React.ReactNode, isLogin?:boolean}) => {
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();
@ -116,7 +114,7 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
const handleProfileMenuClose = ()=>setAnchorEl(null); const handleProfileMenuClose = ()=>setAnchorEl(null);
const isProfileMenuOpened = Boolean(anchorEl); const isProfileMenuOpened = Boolean(anchorEl);
const menuId = 'primary-search-account-menu'; const menuId = 'primary-search-account-menu';
const isLogin = prop.isLogin || false;
const renderProfileMenu = (<Menu const renderProfileMenu = (<Menu
anchorEl={anchorEl} anchorEl={anchorEl}
anchorOrigin={{horizontal:'right',vertical:"top"}} anchorOrigin={{horizontal:'right',vertical:"top"}}
@ -170,15 +168,19 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React
<InputBase placeholder="search" <InputBase placeholder="search"
classes={{ root: classes.inputRoot, input: classes.inputInput }}></InputBase> classes={{ root: classes.inputRoot, input: classes.inputInput }}></InputBase>
</div> </div>
<IconButton {
edge="end" isLogin ?
aria-label="account of current user" <IconButton
aria-controls={menuId} edge="end"
aria-haspopup="true" aria-label="account of current user"
onClick={handleProfileMenuOpen} aria-controls={menuId}
color="inherit"> aria-haspopup="true"
<AccountCircle /> onClick={handleProfileMenuOpen}
</IconButton> color="inherit">
<AccountCircle />
</IconButton>
: <Button color="inherit">Login</Button>
}
</Toolbar> </Toolbar>
</AppBar> </AppBar>
{renderProfileMenu} {renderProfileMenu}

View File

@ -0,0 +1,28 @@
import React, {useState, useEffect} from 'react';
import {Redirect, Route ,Switch,useHistory, useRouteMatch, match as MatchType, Link as RouterLink} from 'react-router-dom';
import { LoadingCircle } from '../component/loading';
import { Link, Paper, makeStyles, Theme, Box, Typography } from '@material-ui/core';
import { Content, makeThumbnailUrl } from '../accessor/contents';
type MangaType = "manga"|"artist cg"|"donjinshi"|"western"
export type PresentableTag = {
artist:string[],
group: string[],
series: string[],
type: MangaType,
character: string[],
tags: string[],
}
export const MangaReader = (props:{content:Content})=>{
const additional = props.content.additional;
if(!('page' in additional)){
console.error("invalid content : page read fail : "+ JSON.stringify(additional));
return <Typography>Error. DB error. page restriction</Typography>
}
const page:number = additional['page'] as number;
return (<div>{[...Array(page).keys()].map(x=>(<img src={`/content/${props.content.id}/manga/${x}`} style={{maxHeight:'100%',maxWidth:'100%'}}></img>))}</div>);
}
export default MangaReader;

View File

@ -0,0 +1,21 @@
import React from 'react';
import { Content, makeThumbnailUrl } from '../accessor/contents';
import {MangaReader} from './manga';
export type PresenterCollection = {
ReaderPage: (props:{content:Content,className?:string})=> JSX.Element;
};
export const getPresenter = (content:Content):PresenterCollection=>{
return {
ReaderPage:MangaReader
};
}
export const ThumbnailContainer = (props:{content:Content, className?:string})=>{
const thumbnailurl = makeThumbnailUrl(props.content);
if(props.content.content_type === "video"){
return (<video src={thumbnailurl} className={props.className}></video>)
}
else return (<img src={thumbnailurl} className={props.className}></img>)
}

View File

View File

@ -2,7 +2,7 @@ const path = require("path");
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = ()=>{return { module.exports = ()=>{return {
entry: './src/client/js/app.tsx', entry: './src/client/app.tsx',
output: { output: {
path: path.resolve(__dirname, 'dist/js'), path: path.resolve(__dirname, 'dist/js'),
filename: 'bundle.js' filename: 'bundle.js'