diff options
Diffstat (limited to 'src/App.js')
-rw-r--r-- | src/App.js | 465 |
1 files changed, 440 insertions, 25 deletions
@@ -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> + </> + ); +} |