aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorEugene Sokolov <eug-vs@keemail.me>2020-08-14 16:58:41 +0300
committerGitHub <noreply@github.com>2020-08-14 16:58:41 +0300
commit9073af9512687e724a5237d55a85656abb3305e5 (patch)
treed4a96f03365fac6f36b3826e2c18c24ee3db0974 /src/components
parenta6d2d1d833116772178125b3e047e0811131c0e0 (diff)
parentd309f375af53dbb3415e2f892dc85e495ea0cf4c (diff)
downloadwhich-ui-9073af9512687e724a5237d55a85656abb3305e5.tar.gz
Merge pull request #82 from which-ecosystem/better-ux
Material images (loading, transitions, error)
Diffstat (limited to 'src/components')
-rw-r--r--src/components/Image/BackgroundImage.tsx35
-rw-r--r--src/components/Image/Image.tsx100
-rw-r--r--src/components/PollCard/PollCard.tsx29
3 files changed, 144 insertions, 20 deletions
diff --git a/src/components/Image/BackgroundImage.tsx b/src/components/Image/BackgroundImage.tsx
new file mode 100644
index 0000000..e749d10
--- /dev/null
+++ b/src/components/Image/BackgroundImage.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import Image from './Image';
+
+interface PropTypes {
+ src?: string;
+ alt?: string;
+}
+
+const useStyles = makeStyles({
+ root: {
+ position: 'absolute',
+ width: '100%',
+ height: '100%'
+ },
+ image: {
+ objectFit: 'cover',
+ pointerEvents: 'none',
+ width: '100%',
+ height: '100%'
+ }
+});
+
+const BackgroundImage: React.FC<PropTypes> = ({ src, alt }) => {
+ const classes = useStyles();
+
+ return (
+ <picture className={classes.root}>
+ <Image src={src} alt={alt} className={classes.image} />
+ </picture>
+ );
+};
+
+export default BackgroundImage;
+
diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx
new file mode 100644
index 0000000..1898716
--- /dev/null
+++ b/src/components/Image/Image.tsx
@@ -0,0 +1,100 @@
+import React, { useState, useCallback, useMemo } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import CircularProgress from '@material-ui/core/CircularProgress';
+import ErrorIcon from '@material-ui/icons/BrokenImage';
+import grey from '@material-ui/core/colors/grey';
+
+interface PropTypes {
+ src?: string;
+ alt?: string;
+ className?: string;
+ animationDuration?: number;
+ disableLoading?: boolean;
+}
+
+type Status = 'success' | 'loading' | 'error';
+
+const useStyles = makeStyles(theme => ({
+ container: {
+ width: '100%',
+ height: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center'
+ },
+ success: {
+ opacity: '100%',
+ filterBrightness: '100%',
+ filterSaturate: '100%'
+ },
+ loading: {
+ opacity: 0,
+ filterBrightness: 0,
+ filterSaturate: '20%',
+ position: 'absolute'
+ },
+ error: {
+ display: 'none'
+ },
+ errorIcon: {
+ color: grey[300],
+ width: theme.spacing(6),
+ height: theme.spacing(6)
+ }
+}));
+
+const Image: React.FC<PropTypes> = React.memo(({
+ src,
+ alt,
+ className,
+ animationDuration = 1000,
+ disableLoading = false
+}) => {
+ const classes = useStyles();
+ const [status, setStatus] = useState<Status>('loading');
+
+ const handleLoad = useCallback((): void => {
+ setStatus('success');
+ }, [setStatus]);
+
+ const handleError = useCallback((): void => {
+ setStatus('error');
+ }, [setStatus]);
+
+ const transition = useMemo(() => `
+ filterBrightness ${animationDuration * 0.75}ms cubic-bezier(0.4, 0.0, 0.2, 1),
+ filterSaturate ${animationDuration}ms cubic-bezier(0.4, 0.0, 0.2, 1),
+ opacity ${animationDuration / 2}ms cubic-bezier(0.4, 0.0, 0.2, 1)
+ `, [animationDuration]);
+
+ const image = (
+ <img
+ src={src}
+ alt={alt}
+ className={`${className} ${classes[status]}`}
+ style={{ transition }}
+ onLoad={handleLoad}
+ onError={handleError}
+ />
+ );
+
+ return (
+ <>
+ {src && image}
+ {
+ status !== 'success' && (
+ <div className={classes.container}>
+ {
+ (status === 'error' || !src)
+ ? <ErrorIcon className={classes.errorIcon} />
+ : (!disableLoading && <CircularProgress color="primary" />)
+ }
+ </div>
+ )
+ }
+ </>
+ );
+});
+
+export default Image;
+
diff --git a/src/components/PollCard/PollCard.tsx b/src/components/PollCard/PollCard.tsx
index 689e872..a4c2144 100644
--- a/src/components/PollCard/PollCard.tsx
+++ b/src/components/PollCard/PollCard.tsx
@@ -1,15 +1,12 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
-import {
- Card,
- CardActionArea,
- CardMedia
-} from '@material-ui/core/';
+import { Card, CardActionArea } from '@material-ui/core/';
import { Which, Poll } from 'which-types';
import { useSnackbar } from 'notistack';
import PercentageBar from './PercentageBar';
import UserStrip from '../UserStrip/UserStrip';
+import BackgroundImage from '../Image/BackgroundImage';
import { post } from '../../requests';
import { useAuth } from '../../hooks/useAuth';
@@ -27,12 +24,10 @@ const DATE_FORMAT = {
};
const useStyles = makeStyles(theme => ({
- images: {
+ media: {
+ display: 'flex',
height: theme.spacing(50)
},
- imagesBlock: {
- display: 'flex'
- },
rateLine: {
position: 'relative',
width: '100%',
@@ -101,19 +96,13 @@ const PollCard: React.FC<PropTypes> = ({ poll, setPoll }) => {
return (
<Card>
<UserStrip user={author} info={date} />
- <div className={classes.imagesBlock}>
- <CardActionArea onDoubleClick={handleLeft}>
- <CardMedia
- className={classes.images}
- image={left.url}
- />
+ <div className={classes.media}>
+ <CardActionArea onDoubleClick={handleLeft} className={classes.media}>
+ <BackgroundImage src={left.url} />
<PercentageBar value={leftPercentage} which="left" like={vote?.which === 'left'} />
</CardActionArea>
- <CardActionArea onDoubleClick={handleRight}>
- <CardMedia
- className={classes.images}
- image={right.url}
- />
+ <CardActionArea onDoubleClick={handleRight} className={classes.media}>
+ <BackgroundImage src={right.url} />
<PercentageBar value={rightPercentage} which="right" like={vote?.which === 'right'} />
</CardActionArea>
</div>