diff options
Diffstat (limited to 'src/App.js')
-rw-r--r-- | src/App.js | 622 |
1 files changed, 0 insertions, 622 deletions
diff --git a/src/App.js b/src/App.js deleted file mode 100644 index f2794dc..0000000 --- a/src/App.js +++ /dev/null @@ -1,622 +0,0 @@ -import React, { useState } 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 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 Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -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 NavigateNextIcon from '@material-ui/icons/NavigateNext'; -import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; -import Grow from '@material-ui/core/Grow'; -import Link from '@material-ui/core/Link'; -import Breadcrumbs from '@material-ui/core/Breadcrumbs'; -import LazyLoad from 'react-lazyload'; -import { CarouselProvider, Slider, Slide, ButtonBack, ButtonNext } from 'pure-react-carousel'; -import useTimeout from 'use-timeout' -import { useWindowDimensions } from './utils.js'; -import 'pure-react-carousel/dist/react-carousel.es.css'; -import 'fontsource-roboto'; - -import { - BrowserRouter as Router, - Switch, - Route, - Redirect, - Link as RouteLink, - useLocation -} from "react-router-dom"; - -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 { height, width } = useWindowDimensions(); - const cols = Math.floor(width / 240); - const classes = makeStyles((theme) => ({ - root: { paddingLeft: cols > 1 ? 18 : 4, paddingRight: cols > 1 ? 18 : 4 }, - controls: { position: 'relative', width: '100%', maxWidth: (cols > 4 ? 640 : '100%') }, - 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' }, - img: { width: '100%', height: (height > 1024 ? 480 : 240), objectFit: 'contain', backgroundImage: 'url(/loading.png)', backgroundRepeat: 'no-repeat', backgroundPosition: 'center' } - }))(); - - return ( - <div className={classes.root}> - <Breadcrumbs aria-label="breadcrumb"> - <Link component={RouteLink} to='/store'>{t('Store')}</Link> - <Typography>author</Typography> - <Typography>id</Typography> - </Breadcrumbs> - <StoreSeparator title='Some Title'/> - <Grid - container - direction="row" - justify="flex-end" - alignItems="flex-start" - > - <Grid item style={{padding: 4}}> - <CarouselProvider - infinite - isPlaying - lockOnWindowScroll - isIntrinsicHeight - naturalSlideWidth={640} - naturalSlideHeight={480} - visibleSlides={1} - totalSlides={4}> - <div className={classes.controls}> - <Slider> - <Slide index={1}><img src='https://img.dlsite.jp/modpub/images2/work/doujin/RJ299000/RJ298293_img_main.jpg' className={classes.img} loading='lazy' alt=''/></Slide> - <Slide index={2}><img src='https://img.dlsite.jp/modpub/images2/work/doujin/RJ299000/RJ298293_img_smp1.jpg' className={classes.img} loading='lazy' alt=''/></Slide> - <Slide index={3}><img src='https://img.dlsite.jp/modpub/images2/work/doujin/RJ299000/RJ298293_img_smp2.jpg' className={classes.img} loading='lazy' alt=''/></Slide> - <Slide index={4}><img src='https://img.dlsite.jp/modpub/images2/work/doujin/RJ299000/RJ298293_img_smp3.jpg' className={classes.img} loading='lazy' alt=''/></Slide> - </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> - </Grid> - <Grid item style={{flexGrow: 1, padding: 4}}> - <Grid item> - <Rating size="large" readOnly defaultValue={5} precision={0.5}/> - </Grid> - <Grid item style={{padding: 4}}> - <Typography variant='subtitle2'>{t('Circle')}: Some Circle</Typography> - </Grid> - <Grid item style={{padding: 4}}> - <Typography variant='subtitle2'>{t('Release')}: 2020年10月24日</Typography> - </Grid> - <Grid item style={{padding: 4}}> - <Typography variant='subtitle2'>{t('Format')}: WAV / mp3同梱</Typography> - </Grid> - <Grid item style={{padding: 4}}> - <Typography variant='subtitle2'>{t('Size')}: 6.09GB</Typography> - </Grid> - <Grid item style={{padding: 4}}> - <StoreSeparator title={t('Authors')}/> - <Chip label={'進行豹'} size='small' style={{margin: 4}}/> - <Chip label={'麻の葉'} size='small' style={{margin: 4}}/> - <Chip label={'黒沢ともよ'} size='small' style={{margin: 4}}/> - </Grid> - <Grid item style={{padding: 4}}> - <StoreSeparator title={t('Keywords')}/> - <Chip label={t('Audio')} size='small' style={{margin: 4}}/> - <Chip label='ASMR' size='small' style={{margin: 4}}/> - <Chip label='Fantasy' size='small' style={{margin: 4}}/> - </Grid> - </Grid> - <Grid item style={{padding: 18, flexGrow: (cols <= 1), minWidth: (cols > 1 ? 260 : '100%')}}> - <Grid item style={{padding: 4}}> - <Grid container direction='row'> - <Grid item style={{flexGrow: 1}}> - <Typography>セール特価:</Typography> - </Grid> - <Grid item> - <Typography>1500円</Typography> - </Grid> - </Grid> - </Grid> - <Grid item style={{padding: 4}}> - <Grid container direction='row'> - <Grid item style={{flexGrow: 1}}> - <Typography>ポイント:</Typography> - </Grid> - <Grid item> - <Typography>200</Typography> - </Grid> - </Grid> - </Grid> - <Grid item style={{padding: 4}}> - <Button variant='contained' size='large' color='primary' style={{width: '100%'}}>カートに入れる</Button> - </Grid> - <Grid item style={{padding: 4}}> - <Button variant='contained' size='large' color='secondary' style={{width: '100%'}}>すぐに購入</Button> - </Grid> - </Grid> - </Grid> - <StoreSeparator title='作品内容'/> - <Typography variant='body2' style={{maxWidth: '64em', padding: 4}}> - 大廃線により鉄路のほとんどが失われてしまった国――日ノ本。 - それでも各地に、大廃線に抗い続ける鉄道事業者はぽつぽつと点在している。、 - そうした鉄道各社・各路線の沿線の魅力を、音という切り口から紹介しようという企画、「蓄音レヱル」が、とある大新聞社の主催によって立ち上げられた。 - 音を探し出して紹介する大役を担うことになるのは、普段はその路線を走る列車の運転制御をサポートしている人形/人形型のモジュール、“レイルロオド”たち。 - - 八ツ城から颯馬仙岱まで、隈元-鹿兒島両県の八ツ城海沿いを縫うように走る第三セクター鉄道事業者、肥薩みかん鉄道を走る紅にも、その依頼が届く。 - - - 「みかん鉄道沿線のいい音がする場所を教えたら――それを全国紙の記事で紹介してもらえるの!? - 得しかないじゃん! ぼく、やるよ!」 - - 旧南颯鉄道――いまは滅びてしまった鉄道事業者の最後の一両の動態機として、解体・廃棄された妹たちとの思い出を胸に、紅は相棒であるあなたの手をとり、音探しの旅へと駆け出します。 - - 颯馬仙岱の大名物であるぴんこ団子を焼くを聞き、出深(いずみ)の飛来地に群れなすツルたちの鳴き声に耳をそばだて、牛之濱の潮溜まりで波と戯れ、日凪久の足湯でくつろいで―― - - 紅とふたりで尋ねる沿線の音たちは、きっとあなたの鼓膜を、こころをここちよく揺すり、やわらかにほぐしてくれるでしょう。 - - まっすぐで純情な紅との音探しの旅。 - どうぞ、そのお耳を澄ませてお楽しみください。 - </Typography> - <StoreSeparator title='ユーザーレビュー'/> - <Typography variant='body2' style={{maxWidth: '64em', padding: 4}}> - なし - </Typography> - </div> - ); -} - -function StoreItemSkeleton(props) { - return ( - <Grow in={true}> - <div className={props.className}> - <div style={{display: 'flex', alignItems: 'center', height: 72, padding: 18}}> - <Skeleton variant='circle' width={48} height={48} style={{marginRight: 18}}/> - <div style={{width: '60%'}}> - <Skeleton width='100%'/> - <Skeleton width='100%'/> - </div> - </div> - <Skeleton variant='rect' width='100%' height={180}/> - </div> - </Grow> - ); -} - -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 ( - <LazyLoad resize once placeholder={<StoreItemSkeleton className={props.className}/>}> - <Grow in={true}> - <Card className={props.className} variant="outlined"> - <CardActionArea component={RouteLink} to={'/store/' + props.data.id}> - <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}} - /> - <div className={classes.media}> - <CardMedia - component='img' - loading='lazy' - image={props.data.img} - title={props.data.title} - style={{height: '100%', backgroundImage:'url(/loading.png)'}} - /> - <Chip label={props.data.price} className={classes.price}/> - <Chip label={t(props.data.cat)} className={classes.cat}/> - <Rating size="small" className={classes.rating} readOnly defaultValue={props.data.rating} precision={0.5}/> - </div> - </CardActionArea> - </Card> - </Grow> - </LazyLoad> - ); -} - -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={tw} - naturalSlideHeight={tw} - visibleSlides={cols} - totalSlides={props.data.length + skeletons.length}> - <div className={classes.controls}> - <Slider> - {props.data.map((item, index) => <Slide index={index} key={index}><StoreItem className={classes.tile} data={item}/></Slide>)} - {skeletons.length ? skeletons.map((item, index) => <Slide index={index} key={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 < Math.max(cols * (Math.floor(height / 240)) - cols * 2, 1); 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 StoreView(props) { - const [state, setState] = useState({data: [], loaded: false}); - useTimeout(() => { - 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()); - 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), - }}) - setState({data: result, loaded: true}); - } - fetchData(); - }, 600); - - return ( - <> - <StoreCarousel name='Featured' data={state.data} loading={!state.loaded}/> - <StoreCarousel name='Popular' data={state.data} loading={!state.loaded}/> - <StoreList name='Latest' data={state.data} loading={!state.loaded}/> - </> - ); -} - -function LibraryView(props) { - const [state, setState] = useState({data: [], loaded: false}); - useTimeout(() => { - 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()); - 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), - }}) - setState({data: result, loaded: true}); - } - fetchData(); - }, 600); - - return ( - <> - <StoreList name='Available' data={state.data} loading={!state.loaded}/> - </> - ); -} - -function AccountView(props) { - return <></>; -} - -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('Keywords')}</Typography> - <Divider variant="middle"/> - <Autocomplete - multiple - id='keywords' - options={keywords} - getOptionLabel={(option) => option} - defaultValue={[]} - renderInput={(params) => <TextField {...params} variant="outlined"/>} - /> - <Typography>{t('Category')}</Typography> - <Divider variant="middle"/> - <FormGroup row style={{marginLeft: 18}}> - {categories.map(cat => - <FormControlLabel control={<Checkbox checked={true} name={cat}/>} - key={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}/>} - key={cat.key} label={t(cat.key)}/>)} - </FormGroup> - </Paper> - </Collapse> - </> - ); -} - -function BottomBar(props) { - const { t } = useTranslation(); - const classes = makeStyles((theme) => ({ - stickyBottom: { - position: 'fixed', - width: '100%', - bottom: 0, - }, - }))(); - return ( - <BottomNavigation className={classes.stickyBottom} value={props.path} showLabels> - <BottomNavigationAction label={t('Store')} value='store' icon={<StoreIcon/>} component={RouteLink} to='/store'/> - <BottomNavigationAction label={t('Library')} value='library' icon={<LibraryBooksIcon/>} component={RouteLink} to='/library'/> - <BottomNavigationAction label={t('Account')} value='account' icon={<AccountCircleIcon/>} component={RouteLink} to='/account'/> - </BottomNavigation> - ); -} - -function TopBar(props) { - const { t } = useTranslation(); - const classes = makeStyles((theme) => ({ - title: { flexGrow: 1 }, - backButton: { marginRight: theme.spacing(1) }, - }))(); - const search = (props.components[1] !== 'account'); - return ( - <AppBar position="fixed"> - <Toolbar variant="dense"> - <Grow in={props.components.length > 2} unmountOnExit> - <IconButton edge="start" color="inherit" aria-label="back" className={classes.backButton} onClick={() => window.history.back()}> - <NavigateBeforeIcon/> - </IconButton> - </Grow> - <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> - <Collapse in={search} timeout='auto' unmountOnExit><ItemSearch/></Collapse> - </AppBar> - ); -} - -function Routing() { - var location = useLocation(); - const components = location.pathname.split('/'); - const classes = makeStyles((theme) => ({ - content: { - paddingTop: 105, - paddingBottom: 65, - minHeight: '100vh', - }, - }))(); - return ( - <> - <TopBar components={components}/> - <Paper className={classes.content} elevation={0} square> - <Switch> - <Route path='/store/:id' component={StoreItemDetails}/> - <Route path='/store' component={StoreView}/> - <Route path='/library' component={LibraryView}/> - <Route path='/account' component={AccountView}/> - <Route path='/'><Redirect to='/store'/></Route> - </Switch> - </Paper> - <BottomBar path={components[1]}/> - </> - ); -} - -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]); - return ( - <> - <CssBaseline/> - <ThemeProvider theme={theme}> - <Router><Routing/></Router> - </ThemeProvider> - </> - ); -} |