diff --git a/src/client/accessor/contents.ts b/src/client/accessor/contents.ts index 790b13f..98ace4e 100644 --- a/src/client/accessor/contents.ts +++ b/src/client/accessor/contents.ts @@ -2,7 +2,8 @@ import {Content, ContentAccessor, ContentContent, QueryListOption} from "../../m import {toQueryString} from './util'; const baseurl = "/content"; -export class ClientContentAccessesor implements ContentAccessor{ +export * from "../../model/contents"; +export class ClientContentAccessor implements ContentAccessor{ async findList(option?: QueryListOption | undefined): Promise{ let res = await fetch(`${baseurl}/search?${option !== undefined ? toQueryString(option) : ""}`); //if(res.status !== 200); @@ -64,4 +65,10 @@ export class ClientContentAccessesor implements ContentAccessor{ const ret = await res.json(); return ret; } -} \ No newline at end of file +} +export const CContentAccessor = new ClientContentAccessor; +export const makeThumbnailUrl = (x: Content)=>{ + return `/content/${x.id}/${x.content_type}/thumbnail`; +} + +export default CContentAccessor; \ No newline at end of file diff --git a/src/client/js/app.tsx b/src/client/app.tsx similarity index 64% rename from src/client/js/app.tsx rename to src/client/app.tsx index 6c866a4..1376fbc 100644 --- a/src/client/js/app.tsx +++ b/src/client/app.tsx @@ -1,11 +1,11 @@ import React from 'react'; import ReactDom from 'react-dom'; import {BrowserRouter, Route, Switch as RouterSwitch} from 'react-router-dom'; -import {Headline} from './headline'; -import {Gallery} from './gallery'; -import {ContentInfo} from './contentinfo'; +import {Headline} from './page/headline'; +import {Gallery} from './page/gallery'; +import {ContentAbout} from './page/contentinfo'; -import '../css/style.css'; +import './css/style.css'; const FooProfile = ()=>
test profile
; const App = ()=> ( @@ -13,10 +13,8 @@ const App = ()=> ( - - - - + +
404 Not Found
diff --git a/src/client/component/loading.tsx b/src/client/component/loading.tsx new file mode 100644 index 0000000..3b4c169 --- /dev/null +++ b/src/client/component/loading.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import {Box, CircularProgress} from '@material-ui/core'; + +export const LoadingCircle = ()=>{ + return (
+ +
); +} \ No newline at end of file diff --git a/src/client/component/tagchip.tsx b/src/client/component/tagchip.tsx new file mode 100644 index 0000000..9e2bb2e --- /dev/null +++ b/src/client/component/tagchip.tsx @@ -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 & {color: string})=>{ + const {color,...rest} = props; + const classes = useTagStyles({color : color !== "default" ? color : "#000"}); + return ; +} + +export const TagChip = (props:Omit & {tagname: string})=>{ + const {tagname,...rest} = props; + return ; +} \ No newline at end of file diff --git a/src/client/js/contentinfo.tsx b/src/client/js/contentinfo.tsx deleted file mode 100644 index b2677eb..0000000 --- a/src/client/js/contentinfo.tsx +++ /dev/null @@ -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(
{match?.url}
) -} \ No newline at end of file diff --git a/src/client/js/gallery.tsx b/src/client/js/gallery.tsx deleted file mode 100644 index e9efea9..0000000 --- a/src/client/js/gallery.tsx +++ /dev/null @@ -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 & {color: string})=>{ - const {color,...rest} = props; - const classes = useTagStyles({color : color !== "default" ? color : "#000"}); - return ; -} - -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({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 (
- -
); - } - else{ - return (
{ - state.content.map(x=>{ - const thumbnail_url = `/content/${x.id}/${x.content_type}/thumbnail`; - return ( - { - x.content_type === "manga" ? : - - } - - - {x.title} - - - {x.tags.map(x=>{ - const tagcolor = getTagColorName(x); - return (); - })} - - - ); - }) - } -
); - } -} \ No newline at end of file diff --git a/src/client/page/contentinfo.tsx b/src/client/page/contentinfo.tsx new file mode 100644 index 0000000..f5d0622 --- /dev/null +++ b/src/client/page/contentinfo.tsx @@ -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({ 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 (Oops. Invalid ID) + } + else if(info.notfound){ + return (Content has been removed.) + } + else if (info.content === undefined) { + return (); + } + else return ( + + + + + + + +
404 Not Found invalid url : {prop.match.path}
+
+
); +} + +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 ( + + + + + + {content.title} + + + Artist + {artists.join(", ")} + Tags + + {allTag.map(x => { + return (); + })} + + + + ); +} \ No newline at end of file diff --git a/src/client/page/gallery.tsx b/src/client/page/gallery.tsx new file mode 100644 index 0000000..799a725 --- /dev/null +++ b/src/client/page/gallery.tsx @@ -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({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 (); + } + else{ + return (
{ + state.content.map(x=>{ + const thumbnail_url = makeThumbnailUrl(x); + return ( + + + + + + {x.title} + + + {x.tags.map(x=>{ + return (); + })} + + + ); + }) + } +
); + } +} \ No newline at end of file diff --git a/src/client/js/headline.tsx b/src/client/page/headline.tsx similarity index 89% rename from src/client/js/headline.tsx rename to src/client/page/headline.tsx index 77c7e23..cf7be25 100644 --- a/src/client/js/headline.tsx +++ b/src/client/page/headline.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { Button, CssBaseline, Divider, IconButton, List, ListItem, Drawer, AppBar, Toolbar, Typography, InputBase, ListItemIcon, ListItemText, Menu, MenuItem, - Hidden, useMediaQuery, Tooltip + Hidden, 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'; @@ -45,13 +45,11 @@ const useStyles = makeStyles((theme: Theme) => ({ }, }, toolbar: { - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-end', - padding: theme.spacing(0, 1), ...theme.mixins.toolbar, }, content: { + display:'flex', + flexFlow: 'column', flexGrow: 1, 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 [anchorEl, setAnchorEl] = React.useState(null); const classes = useStyles(); @@ -116,7 +114,7 @@ export const Headline = (prop: { children?: React.ReactNode, navListItem?: React const handleProfileMenuClose = ()=>setAnchorEl(null); const isProfileMenuOpened = Boolean(anchorEl); const menuId = 'primary-search-account-menu'; - + const isLogin = prop.isLogin || false; const renderProfileMenu = ( - - - + { + isLogin ? + + + + : + } {renderProfileMenu} diff --git a/src/client/presenter/manga.tsx b/src/client/presenter/manga.tsx new file mode 100644 index 0000000..e3b002f --- /dev/null +++ b/src/client/presenter/manga.tsx @@ -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 Error. DB error. page restriction + } + const page:number = additional['page'] as number; + return (
{[...Array(page).keys()].map(x=>())}
); +} + +export default MangaReader; \ No newline at end of file diff --git a/src/client/presenter/presenter.tsx b/src/client/presenter/presenter.tsx new file mode 100644 index 0000000..c3330a3 --- /dev/null +++ b/src/client/presenter/presenter.tsx @@ -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 () + } + else return () +} \ No newline at end of file diff --git a/src/client/presenter/video.tsx b/src/client/presenter/video.tsx new file mode 100644 index 0000000..e69de29 diff --git a/webpack.config.js b/webpack.config.js index 75c6c1a..9ef04f9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -2,7 +2,7 @@ const path = require("path"); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); module.exports = ()=>{return { - entry: './src/client/js/app.tsx', + entry: './src/client/app.tsx', output: { path: path.resolve(__dirname, 'dist/js'), filename: 'bundle.js'