add gallery page

This commit is contained in:
monoid 2021-01-05 04:02:43 +09:00
parent 5864bc1547
commit 1fd3c974e0
8 changed files with 456 additions and 98 deletions

View File

@ -11,6 +11,15 @@
"start": "ts-node src/server.ts",
"check-types": "tsc"
},
"browserslist":{
"production":[
"> 10%"
],
"development":[
"last 1 chrome version",
"last 1 firefox version"
]
},
"author": "",
"license": "ISC",
"dependencies": {
@ -24,6 +33,7 @@
"node-stream-zip": "^1.12.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"sqlite3": "^5.0.0",
"ts-node": "^9.1.1"
},
@ -41,6 +51,7 @@
"@types/node": "^14.14.16",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-router-dom": "^5.1.7",
"babel-core": "^6.26.3",
"babel-loader": "^8.2.2",
"css-loader": "^5.0.1",

View File

@ -0,0 +1,67 @@
import {Content, ContentAccessor, ContentContent, QueryListOption} from "../../model/contents";
import {toQueryString} from './util';
const baseurl = "/content";
export class ClientContentAccessesor implements ContentAccessor{
async findList(option?: QueryListOption | undefined): Promise<Content[]>{
let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`);
//if(res.status !== 200);
let ret = await res.json();
return ret;
}
async findById(id: number, tagload?: boolean | undefined): Promise<Content | undefined>{
let res = await fetch(`${baseurl}/${id}`);
if(res.status !== 200) return undefined;
let ret = await res.json();
return ret;
}
/**
* not implement
*/
async findListByBasePath(basepath: string): Promise<Content[]>{
throw new Error("");
return [];
}
async update(c: Partial<Content> & { id: number; }): Promise<boolean>{
const {id,...rest} = c;
const res = await fetch(`${baseurl}/${id}`,{
method: "POST",
body: JSON.stringify(rest)
});
const ret = await res.json();
return ret;
}
async add(c: ContentContent): Promise<number>{
const res = await fetch(`${baseurl}`,{
method: "POST",
body: JSON.stringify(c)
});
const ret = await res.json();
return ret;
}
async del(id: number): Promise<boolean>{
const res = await fetch(`${baseurl}/${id}`,{
method: "DELETE"
});
const ret = await res.json();
return ret;
}
async addTag(c: Content, tag_name: string): Promise<boolean>{
const {id,...rest} = c;
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
method: "POST",
body: JSON.stringify(rest)
});
const ret = await res.json();
return ret;
}
async delTag(c: Content, tag_name: string): Promise<boolean>{
const {id,...rest} = c;
const res = await fetch(`${baseurl}/${id}/tags/${tag_name}`,{
method: "DELETE",
body: JSON.stringify(rest)
});
const ret = await res.json();
return ret;
}
}

View File

@ -2,11 +2,14 @@
type Representable = string|number|boolean;
type ToQueryStringA = {
[name:string]:Representable|Representable[]
[name:string]:Representable|Representable[]|undefined
};
export const toQueryString = (obj:ToQueryStringA)=> {
return Object.entries(obj).map(e =>
return Object.entries(obj)
.filter((e): e is [string,Representable|Representable[]] =>
e[1] !== undefined)
.map(e =>
e[1] instanceof Array
? e[1].map(f=>`${e[0]}[]=${encodeURIComponent(f)}`).join('&')
: `${e[0]}=${encodeURIComponent(e[1])}`)

View File

@ -1,11 +1,24 @@
import hello from './hello'
import React from 'react';
import ReactDom from 'react-dom';
import {Headline} from './test';
import {BrowserRouter, Route} from 'react-router-dom';
import {Headline} from './headline';
import {Gallery} from './gallery';
import style from '../css/style.css';
hello();
const App = ()=> (
<BrowserRouter>
<Headline>
<Route path="/" exact>
<Gallery></Gallery>
</Route>
<Route path="/profile"><div>test profile</div></Route>
</Headline>
</BrowserRouter>
);
ReactDom.render(
<Headline />,
<App/>,
document.getElementById("root")
)

150
src/client/js/gallery.tsx Normal file
View File

@ -0,0 +1,150 @@
import { Box, CircularProgress, Typography, Paper, Chip, withStyles, colors } 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';
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: 200,
height: 200,
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',
},
tag_list:{
display: 'flex',
justifyContent: 'flex-start',
flexWrap: 'wrap',
'& > *': {
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}>
<a className={classes.table_thumbnail}>{
x.content_type === "manga" ? <img className={classes.content_thumnail} src={thumbnail_url}></img> :
<video className={classes.content_thumnail} src={thumbnail_url}></video>
}</a>
<Box className={classes.content_info}>
<Typography variant="h5" style={{marginLeft:theme.spacing(2)}}>{x.title}</Typography>
<Box className={classes.tag_list}>
{x.tags.map(x=>{
const tagcolor = getTagColorName(x);
console.log(tagcolor);
return (<TagChip key={x} label={x} clickable={true} color={tagcolor}></TagChip>);
})}
</Box>
</Box>
</Paper>);
})
}
</div>);
}
}

206
src/client/js/headline.tsx Normal file
View File

@ -0,0 +1,206 @@
import ReactDom from 'react-dom';
import React, { useState } from 'react';
import {
Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer,
AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem,
Hidden, useMediaQuery, Tooltip
} from '@material-ui/core';
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 {Link} from 'react-router-dom';
const drawerWidth = 240;
const useStyles = makeStyles((theme: Theme) => ({
root: {
display: 'flex'
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
},
menuButton: {
marginRight: 36
},
hide: {
display: "none"
},
drawer: {
flexShrink: 0,
whiteSpace: "nowrap",
[theme.breakpoints.up("sm")]:{
width: drawerWidth,
},
},
drawerPaper: {
width: drawerWidth
},
drawerClose: {
overflowX: 'hidden',
[theme.breakpoints.up("sm")]:{
width: theme.spacing(7) + 1,
},
},
toolbar: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
title: {
display: 'none',
[theme.breakpoints.up("sm")]: {
display: 'block'
}
},
search: {
position: 'relative',
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
'&:hover': {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginRight: theme.spacing(2),
marginLeft: 0,
width: '100%',
[theme.breakpoints.up('sm')]: {
marginLeft: theme.spacing(3),
width: 'auto',
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: '100%',
position: 'absolute',
pointerEvents: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
inputRoot: {
color: 'inherit'
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
transition: theme.transitions.create('width'),
width: '100%',
[theme.breakpoints.up('md')]: {
width: '20ch',
"&:hover": {
width: '25ch'
}
},
},
grow: {
flexGrow: 1,
},
}));
export const Headline = (prop: { children?: React.ReactNode }) => {
const [v, setv] = useState(false);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const classes = useStyles();
const theme = useTheme();
const toggleV = () => setv(!v);
const handleProfileMenuOpen = (e:React.MouseEvent<HTMLElement>)=>setAnchorEl(e.currentTarget);
const handleProfileMenuClose = ()=>setAnchorEl(null);
const isProfileMenuOpened = Boolean(anchorEl);
const menuId = 'primary-search-account-menu';
const renderProfileMenu = (<Menu
anchorEl={anchorEl}
anchorOrigin={{horizontal:'right',vertical:"top"}}
id={menuId}
open={isProfileMenuOpened}
keepMounted
transformOrigin={{horizontal:'right',vertical:"top"}}
onClose={handleProfileMenuClose}
>
<MenuItem component={Link} to='/profile'>Profile</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>);
const drawer_contents = (<>
<div className={classes.toolbar}>
<IconButton onClick={toggleV}>
{theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />}
</IconButton>
</div>
<Divider />
<List>
<ListItem button key="Back">
<ListItemIcon>
<Tooltip title="back" placement="bottom">
<ArrowBackIcon></ArrowBackIcon>
</Tooltip>
</ListItemIcon>
<ListItemText primary="Back"></ListItemText>
</ListItem>
</List>
</>);
return (<div className={classes.root}>
<CssBaseline />
<AppBar position="fixed" className={classes.appBar}>
<Toolbar>
<IconButton color="inherit"
aria-label="open drawer"
onClick={toggleV}
edge="start"
className={classes.menuButton}>
<MenuIcon></MenuIcon>
</IconButton>
<Typography variant="h5" noWrap className={classes.title}>
Ionian
</Typography>
<div className={classes.grow}></div>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase placeholder="search"
classes={{ root: classes.inputRoot, input: classes.inputInput }}></InputBase>
</div>
<IconButton
edge="end"
aria-label="account of current user"
aria-controls={menuId}
aria-haspopup="true"
onClick={handleProfileMenuOpen}
color="inherit">
<AccountCircle />
</IconButton>
</Toolbar>
</AppBar>
{renderProfileMenu}
<nav>
<Hidden smUp implementation="css">
<Drawer variant="temporary" anchor='left' open={v} onClose={toggleV}
classes={{paper: classes.drawerPaper}}>
{drawer_contents}
</Drawer>
</Hidden>
<Hidden xsDown implementation="css">
<Drawer variant='permanent' anchor='left' className={[classes.drawer, classes.drawerClose].join(" ").trim()} classes={{
paper: classes.drawerClose
}}>
{drawer_contents}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar}></div>
{prop.children}
</main>
</div>);
};
export default Headline;

View File

@ -1,4 +0,0 @@
export default function (){
console.log("hello");
console.log("???");
};

View File

@ -1,88 +0,0 @@
import ReactDom from 'react-dom';
import React, { useState } from 'react';
import Drawer from '@material-ui/core/Drawer';
import { Button, Divider, IconButton, List, ListItem } from '@material-ui/core';
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
import {ChevronLeft, ChevronRight} from '@material-ui/icons';
const drawerWidth = 240;
const useStyles = makeStyles((theme: Theme) => ({
root: {
display: 'flex'
},
appBar: {
transition: theme.transitions.create(['width', 'margin'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
},
appBarShift: {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawer: {
width: drawerWidth,
flexShrink: 0,
},
drawerPaper: {
width: drawerWidth,
},
drawerHeader: {
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: 'flex-end',
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: -drawerWidth,
},
contentShift: {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
},
}));
export const Headline = () => {
const [v, setv] = useState(false);
const classes = useStyles();
const theme = useTheme();
return (<div className={classes.root}>
<Drawer variant='persistent' anchor='left' open={v} className={classes.drawer} classes={{paper:classes.drawerPaper}}>
<div className={classes.drawerHeader}>
<IconButton onClick={()=>setv(false)}>
{theme.direction === "ltr" ? <ChevronLeft /> : <ChevronRight />}
</IconButton>
</div>
<Divider />
<List>
<ListItem>
<div>NO</div>
</ListItem>
</List>
</Drawer>
<main className={([classes.content, v ? classes.contentShift: ""].join(" ").trim())}>
<h1>aaa{`a${v} ${classes.content}`}aaa</h1>
<Button onClick={() => setv(!v)}>open</Button>
</main>
</div>);
};
export default Headline;