summaryrefslogtreecommitdiff
path: root/src/App.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/App.js')
-rw-r--r--src/App.js465
1 files changed, 440 insertions, 25 deletions
diff --git a/src/App.js b/src/App.js
index 3784575..a02ad84 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,25 +1,440 @@
-import logo from './logo.svg';
-import './App.css';
-
-function App() {
- return (
- <div className="App">
- <header className="App-header">
- <img src={logo} className="App-logo" alt="logo" />
- <p>
- Edit <code>src/App.js</code> and save to reload.
- </p>
- <a
- className="App-link"
- href="https://reactjs.org"
- target="_blank"
- rel="noopener noreferrer"
- >
- Learn React
- </a>
- </header>
- </div>
- );
-}
-
-export default App;
+import React, { useState, useEffect } from "react";
+import he from 'he';
+import { useTranslation } from "react-i18next";
+import { makeStyles, withStyles, createMuiTheme, ThemeProvider } from '@material-ui/core/styles';
+import useMediaQuery from '@material-ui/core/useMediaQuery';
+import Autocomplete from '@material-ui/lab/Autocomplete';
+import TextField from '@material-ui/core/TextField';
+import red from '@material-ui/core/colors/red';
+import CssBaseline from '@material-ui/core/CssBaseline';
+import Box from '@material-ui/core/Box';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import Typography from '@material-ui/core/Typography';
+import Checkbox from '@material-ui/core/Checkbox';
+import FormGroup from '@material-ui/core/FormGroup';
+import FormControlLabel from '@material-ui/core/FormControlLabel';
+import IconButton from '@material-ui/core/IconButton';
+import MenuIcon from '@material-ui/icons/Menu';
+import MenuOpenIcon from '@material-ui/icons/MenuOpen';
+import Avatar from '@material-ui/core/Avatar';
+import Collapse from '@material-ui/core/Collapse';
+import Card from '@material-ui/core/Card';
+import CardHeader from '@material-ui/core/CardHeader';
+import CardActionArea from '@material-ui/core/CardActionArea';
+import CardMedia from '@material-ui/core/CardMedia';
+import GridList from '@material-ui/core/GridList';
+import GridListTile from '@material-ui/core/GridListTile';
+import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
+import SearchIcon from '@material-ui/icons/Search';
+import Badge from '@material-ui/core/Badge';
+import InputBase from '@material-ui/core/InputBase';
+import BottomNavigation from '@material-ui/core/BottomNavigation';
+import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
+import Skeleton from '@material-ui/lab/Skeleton';
+import Paper from '@material-ui/core/Paper';
+import Divider from '@material-ui/core/Divider';
+import Chip from '@material-ui/core/Chip';
+import Rating from '@material-ui/lab/Rating';
+import AccountCircleIcon from '@material-ui/icons/AccountCircle';
+import StoreIcon from '@material-ui/icons/Store';
+import LibraryBooksIcon from '@material-ui/icons/LibraryBooks';
+import HelpOutlineIcon from '@material-ui/icons/HelpOutline';
+import Button from '@material-ui/core/Button';
+import NavigateNextIcon from '@material-ui/icons/NavigateNext';
+import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore';
+import Grow from '@material-ui/core/Grow';
+import { CarouselProvider, Slider, Slide, ButtonBack, ButtonNext, DotGroup } from 'pure-react-carousel';
+import { useWindowDimensions } from './utils.js';
+import 'pure-react-carousel/dist/react-carousel.es.css';
+import 'fontsource-roboto';
+
+const StyledBadge = withStyles((theme) => ({
+ badge: {
+ right: -3,
+ top: 13,
+ border: `2px solid ${theme.palette.background.paper}`,
+ padding: '0 4px',
+ },
+}))(Badge);
+
+function StoreItemDetails(props) {
+ const { t } = useTranslation();
+ const classes = makeStyles((theme) => ({
+ }))();
+
+ return (
+ <>
+ <Typography variant="h6" gutterBottom>
+ {props.data.title}
+ </Typography>
+ <Typography variant="body2" color="textSecondary" component="p">
+ {props.data.desc}
+ </Typography>
+ </>
+ );
+}
+
+function StoreItem(props) {
+ const { t } = useTranslation();
+ const classes = makeStyles((theme) => ({
+ media: { height: 180, position: 'relative' },
+ cat: { position: 'absolute', bottom: 4, left: 4 },
+ price: { position: 'absolute', bottom: 4, right: 4 },
+ rating: {
+ position: 'absolute', top: 4, right: 4,
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
+ padding: 4, borderRadius: 4,
+ },
+ content: { paddingLeft: 8 },
+ text: {
+ textOverflow: 'ellipsis',
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+ },
+ avatar: { backgroundColor: red[500] }
+ }))();
+
+ return (
+ <>
+ <Grow in={true}>
+ <Card className={props.className} variant="outlined">
+ <CardActionArea>
+ <CardHeader
+ avatar={
+ <Avatar aria-label={props.data.author} className={classes.avatar}>
+ {props.data.author.substring(0, 2)}
+ </Avatar>
+ }
+ title={props.data.title}
+ titleTypographyProps={{className: classes.text}}
+ subheader={props.data.author}
+ subheaderTypographyProps={{className: classes.text}}
+ />
+ <CardMedia className={classes.media}
+ image={props.data.img}
+ title={props.data.title}
+ >
+ <Chip label={props.data.price} className={classes.price}/>
+ <Chip label={t(props.data.cat)} className={classes.cat}/>
+ <Rating name="size-small" size="small" className={classes.rating} readOnly defaultValue={props.data.rating} precision={0.5}/>
+ </CardMedia>
+ </CardActionArea>
+ </Card>
+ </Grow>
+ </>
+ );
+}
+
+function StoreItemSkeleton(props) {
+ return (
+ <Grow in={true}>
+ <div className={props.className}>
+ <Box display="flex" alignItems="center">
+ <Box margin={1}>
+ <Skeleton variant="circle">
+ <Avatar/>
+ </Skeleton>
+ </Box>
+ <Box width="100%">
+ <Skeleton width="100%">
+ <Typography>.</Typography>
+ </Skeleton>
+ </Box>
+ </Box>
+ <Skeleton variant="rect" width="100%">
+ <div style={{ paddingTop: '200px' }} />
+ </Skeleton>
+ </div>
+ </Grow>
+ );
+}
+
+function StoreSeparator(props) {
+ const { t } = useTranslation();
+ const classes = makeStyles((theme) => ({
+ root: { margin: 4 },
+ }))();
+ return (
+ <div className={classes.root}>
+ <Typography variant="h6">{t(props.title)}</Typography>
+ <Divider variant="middle"/>
+ </div>
+ );
+}
+
+function StoreCarousel(props) {
+ const { height, width } = useWindowDimensions();
+ const cols = Math.floor(width / 240);
+ const tw = (cols > 1 ? (cols * 240 > width - 18 ? 240 - 20 : 240) : '100%')
+ const classes = makeStyles((theme) => ({
+ root: { paddingLeft: cols > 1 ? 18 : 4, paddingRight: cols > 1 ? 18 : 4 },
+ tile: { width: tw, marginRight: 'auto', marginLeft: 'auto' },
+ controls: { position: 'relative' },
+ prev: { position: 'absolute', left: cols > 1 ? -15 : -4, top: '50%', backgroundColor: 'rgba(0, 0, 0, 0.5)', color: 'white' },
+ next: { position: 'absolute', right: cols > 1 ? -15 : -4, top: '50%', backgroundColor: 'rgba(0, 0, 0, 0.5)', color: 'white' },
+ }))();
+ var skeletons = [];
+ for (var i = 0; props.loading && i < cols; i += 1) skeletons.push({});
+ return (
+ <div className={classes.root}>
+ <StoreSeparator title={props.name}/>
+ <CarouselProvider
+ infinite
+ isPlaying
+ lockOnWindowScroll
+ isIntrinsicHeight
+ naturalSlideWidth={240}
+ naturalSlideHeight={240}
+ visibleSlides={cols}
+ totalSlides={props.data.length + skeletons.length}>
+ <div className={classes.controls}>
+ <Slider>
+ {props.data.map((item, index) => <Slide index={index}><StoreItem className={classes.tile} data={item}/></Slide>)}
+ {skeletons.length ? skeletons.map((item, index) => <Slide index={index}><StoreItemSkeleton className={classes.tile}/></Slide>) : null}
+ </Slider>
+ <IconButton size='small' aria-label='previous' component={ButtonBack} className={classes.prev}>
+ <NavigateBeforeIcon/>
+ </IconButton>
+ <IconButton size='small' aria-label='next' component={ButtonNext} className={classes.next}>
+ <NavigateNextIcon/>
+ </IconButton>
+ </div>
+ </CarouselProvider>
+ </div>
+ );
+}
+
+function StoreList(props) {
+ const { height, width } = useWindowDimensions();
+ const cols = Math.floor(width / 240);
+ const tw = (cols > 1 ? (cols * 240 > width - 18 ? 240 - 20 : 240) : '100%')
+ const classes = makeStyles((theme) => ({
+ list: { justifyContent: 'space-evenly', maxWidth: '100%' },
+ tile: { width: tw, marginRight: 'auto', marginLeft: 'auto' },
+ }))();
+ var extra = [];
+ for (var i = 0; i < cols - (props.data.length % cols); i += 1) extra.push({key: i});
+ var skeletons = [];
+ for (var i2 = 0; props.loading && i2 < cols * (Math.floor(height / 240)) - cols * 2; i2 += 1) skeletons.push({key: i2});
+ return (
+ <div className={props.className} style={{ paddingLeft: cols > 1 ? 18 : 4, paddingRight: cols > 1 ? 18 : 4 }}>
+ <StoreSeparator title={props.name}/>
+ <GridList className={classes.list} cols={cols} cellHeight='auto'>
+ {props.data.map(item => <GridListTile key={item.img}><StoreItem data={item} className={classes.tile}/></GridListTile>)}
+ {extra.map(item => <GridListTile className={classes.tile} key={item.key}/>)}
+ {skeletons.map(item => <GridListTile key={item.key}><StoreItemSkeleton className={classes.tile}/></GridListTile>)}
+ </GridList>
+ </div>
+ );
+}
+
+function ItemSearch(props) {
+ const { t } = useTranslation();
+
+ const classes = makeStyles((theme) => ({
+ search: {
+ padding: '2px 4px',
+ display: 'flex',
+ alignItems: 'center',
+ },
+ input: {
+ marginLeft: theme.spacing(1),
+ flex: 1,
+ },
+ iconButton: {
+ padding: 10,
+ },
+ }))();
+
+ const categories = ['Manga', 'CG', 'Audio', 'Video', 'Game']
+ const audience = [{key: 'All ages', value: true}, {key: 'Heterosexual', value: false}, {key: 'Homosexual', value: false}]
+
+ const keywords = [
+ t('RPG'),
+ t('Simulation'),
+ t('Stragety'),
+ t('Action'),
+ t('ASMR'),
+ t('Anime'),
+ t('3D'),
+ t('2D'),
+ t('Pixel art'),
+ t('Live action'),
+ t('Metal'),
+ t('Pop'),
+ t('Rock'),
+ ];
+
+ const [menuOpen, setMenuOpen] = useState(false);
+
+ return (
+ <>
+ <Paper component="form" className={classes.search} variant="outlined" square>
+ <IconButton className={classes.iconButton} aria-label="filter" onClick={() => setMenuOpen(!menuOpen)}>
+ {menuOpen ? <MenuOpenIcon/> : <MenuIcon/>}
+ </IconButton>
+ <InputBase
+ className={classes.input}
+ placeholder={t('Search')}
+ inputProps={{ 'aria-label': 'search' }}
+ />
+ <IconButton type="submit" className={classes.iconButton} aria-label="search">
+ <SearchIcon/>
+ </IconButton>
+ </Paper>
+ <Collapse in={menuOpen} timeout="auto" unmountOnExit>
+ <Paper elevation={1} square style={{padding: 4, paddingLeft: 18, paddingRight: 18}}>
+ <Typography>{t('Category')}</Typography>
+ <Divider variant="middle"/>
+ <FormGroup row style={{marginLeft: 18}}>
+ {categories.map(cat =>
+ <FormControlLabel control={<Checkbox checked={true} name={cat}/>}
+ label={t(cat)}/>)}
+ </FormGroup>
+ <Typography>{t('Audience')}</Typography>
+ <Divider variant="middle"/>
+ <FormGroup row style={{marginLeft: 18}}>
+ {audience.map(cat =>
+ <FormControlLabel control={<Checkbox checked={cat.value} name={cat.key}/>}
+ label={t(cat.key)}/>)}
+ </FormGroup>
+ <Typography>{t('Keywords')}</Typography>
+ <Divider variant="middle"/>
+ <Autocomplete
+ multiple
+ id='keywords'
+ options={keywords}
+ getOptionLabel={(option) => option}
+ defaultValue={[]}
+ renderInput={(params) => <TextField {...params} variant="outlined"/>}
+ />
+ </Paper>
+ </Collapse>
+ </>
+ );
+}
+
+function StoreView(props) {
+ const { t } = useTranslation();
+
+ const [data, setData] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+ useEffect(() => {
+ async function fetchData() {
+ function getRandomInt(max) {
+ return Math.floor(Math.random() * Math.floor(max));
+ }
+ var result = await fetch('https://cors-anywhere.herokuapp.com/https://www.eisys-bcs.jp/data.json?key[]=dlsite-doujin_home_center2').then(result => result.json());
+ console.log(result)
+ result = result.data.['dlsite-doujin_home_center2'].banners.map(item => { return {
+ id: 'no-id',
+ author: he.decode(item.title),
+ title: he.decode(item.title),
+ shortdesc: 'foobar',
+ desc: 'foobar',
+ img: 'https:' + item.ssl_path,
+ cat: 'Misc',
+ price: getRandomInt(3000) + '円',
+ rating: getRandomInt(5),
+ }})
+ setData(result);
+ setLoaded(true)
+ }
+ fetchData();
+ }, []);
+
+ const classes = makeStyles((theme) => ({
+ root: {
+ paddingBottom: '65px',
+ },
+ stickyBottom: {
+ top: 'auto',
+ bottom: 0,
+ },
+ search: {
+ padding: '2px 4px',
+ display: 'flex',
+ alignItems: 'center',
+ },
+ input: {
+ marginLeft: theme.spacing(1),
+ flex: 1,
+ },
+ iconButton: {
+ padding: 10,
+ },
+ title: { flexGrow: 1 },
+ }))();
+
+ return (
+ <>
+ <AppBar position="sticky">
+ <Toolbar variant="dense">
+ <Typography variant="h6" className={classes.title}>
+ {t('DoujinSea')}
+ </Typography>
+ <IconButton aria-label="help">
+ <HelpOutlineIcon />
+ </IconButton>
+ <IconButton aria-label="cart">
+ <StyledBadge badgeContent={4} color="secondary">
+ <ShoppingCartIcon />
+ </StyledBadge>
+ </IconButton>
+ </Toolbar>
+ <ItemSearch/>
+ </AppBar>
+ <Paper className={classes.root} elevation={0} square>
+ <StoreCarousel name='Featured' data={data} loading={!loaded}/>
+ <StoreCarousel name='Popular' data={data} loading={!loaded}/>
+ <StoreList name='Latest' data={data} loading={!loaded}/>
+ </Paper>
+ <AppBar position="fixed" className={classes.stickyBottom}>
+ <BottomNavigation
+ value='store'
+ onChange={(event, newValue) => {}}
+ showLabels
+ >
+ <BottomNavigationAction label={t('Store')} value='store' icon={<StoreIcon/>} />
+ <BottomNavigationAction label={t('Library')} value='library' icon={<LibraryBooksIcon/>} />
+ <BottomNavigationAction label={t('Account')} value='account' icon={<AccountCircleIcon/>} />
+ </BottomNavigation>
+ </AppBar>
+ </>
+ )
+}
+
+export default function Main() {
+ const prefersDarkMode = false; // useMediaQuery('(prefers-color-scheme: dark)');
+ const theme = React.useMemo(() => createMuiTheme({
+ palette: {
+ type: prefersDarkMode ? 'dark' : 'light',
+ primary: red,
+ },
+ }), [prefersDarkMode]);
+
+ const GlobalCss = withStyles({
+ '@global': {
+ '#keywords-popup': {
+ padding: 0,
+ },
+ '.KeywordList li': {
+ display: 'inline-block',
+ verticalAlign: 'top',
+ marginLeft: 4,
+ marginRight: 4,
+ },
+ }
+ })(() => null);
+
+ return (
+ <>
+ <CssBaseline/>
+ <GlobalCss />
+ <ThemeProvider theme={theme}>
+ <StoreView/>
+ </ThemeProvider>
+ </>
+ );
+}