@mui/system#Box TypeScript Examples

The following examples show how to use @mui/system#Box. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: AboutPage.tsx    From frontend with MIT License 6 votes vote down vote up
export default function AboutPage() {
  const { t } = useTranslation()
  return (
    <Layout title={t('about:about.title')} metaDescription={t('about:about.description')}>
      <PrinciplesThatUniteUs />
      <HowEveryThingBegin />
      <Box textAlign="center" m={6}>
        <LinkButton color="primary" size="large" variant="contained" href={routes.support}>
          {t('nav.about.support-us')}
        </LinkButton>
      </Box>
    </Layout>
  )
}
Example #2
Source File: StatFilterCard.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
export default function StatFilterCard({ statFilters = {}, setStatFilters, disabled = false }:
  { statFilters: Dict<StatKey, number>, setStatFilters: (object: Dict<StatKey, number>) => void, disabled?: boolean }) {
  const { t } = useTranslation("page_character")
  const { data } = useContext(DataContext)
  const statKeys: StatKey[] = ["atk", "hp", "def", "eleMas", "critRate_", "critDMG_", "heal_", "enerRech_"]
  if (data.get(input.weaponType).value !== "catalyst") statKeys.push("physical_dmg_")
  const charEle = data.get(input.charEle).value as ElementKey
  statKeys.push(`${charEle}_dmg_`)

  const remainingKeys = statKeys.filter(key => !(Object.keys(statFilters) as any).some(k => k === key))
  const setFilter = useCallback((sKey, min) => setStatFilters({ ...statFilters, [sKey]: min }), [statFilters, setStatFilters],)
  return <Box>
    <CardLight>
      <CardContent sx={{ display: "flex", gap: 1, justifyContent: "space-between" }}>
        <Typography>{t`tabOptimize.constraintFilter.title`}</Typography>
        <InfoTooltip title={<Typography>{t`tabOptimize.constraintFilter.tooltip`}</Typography>} />
      </CardContent>
    </CardLight>
    <Box display="flex" flexDirection="column" gap={0.5}>
      {Object.entries(statFilters).map(([statKey, min]) => {
        return <StatFilterItem key={statKey} statKey={statKey} statKeys={remainingKeys} setFilter={setFilter} disabled={disabled} value={min} close={() => {
          delete statFilters[statKey]
          setStatFilters({ ...statFilters })
        }} />
      })}
      <StatFilterItem value={undefined} close={undefined} statKeys={remainingKeys} setFilter={setFilter} disabled={disabled} />
    </Box>
  </Box>
}
Example #3
Source File: CheckBoxFilter.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 6 votes vote down vote up
export default function CheckBoxFilter(props: Props) {
    const {
        state,
        name,
        position,
        group,
        updateFilterValue,
        update,
    } = props;
    const [val, setval] = React.useState(state);

    const handleChange = (event: { target: { name: any; checked: any; }; }) => {
        setval(event.target.checked);
        const upd = update.filter((e: {
            position: number; group: number | undefined;
        }) => !(position === e.position && group === e.group));
        updateFilterValue([...upd, { position, state: event.target.checked.toString(), group }]);
    };

    if (state !== undefined) {
        return (
            <Box sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
                <FormControlLabel
                    key={name}
                    control={(
                        <Checkbox
                            name={name}
                            checked={val}
                            onChange={handleChange}
                        />
                    )}
                    label={name}
                />
            </Box>
        );
    }
    return (<></>);
}
Example #4
Source File: TabTeambuffs.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
export default function TabTeambuffs() {
  return <Box display="flex" flexDirection="column" gap={1} alignItems="stretch">
    <Grid container spacing={1}>
      <Grid item xs={12} md={6} lg={3} sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
        <TeamBuffDisplay />
        <ResonanceDisplay />
      </Grid>
      {range(0, 2).map(i => <Grid item xs={12} md={6} lg={3} key={i}>
        <TeammateDisplay index={i} />
      </Grid>)}
    </Grid>
  </Box>
}
Example #5
Source File: PageNumber.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 6 votes vote down vote up
export default function PageNumber(props: IProps) {
    const { settings, curPage, pageCount } = props;

    return (
        <Box sx={{
            display: settings.showPageNumber ? 'block' : 'none',
            position: 'fixed',
            bottom: '50px',
            right: settings.staticNav ? 'calc((100vw - 325px)/2)' : 'calc((100vw - 25px)/2)',
            padding: '2px',
            paddingLeft: '4px',
            paddingRight: '4px',
            textAlign: 'center',
            backgroundColor: 'rgba(0, 0, 0, 0.3)',
            borderRadius: '10px',
        }}
        >
            {`${curPage + 1} / ${pageCount}`}
        </Box>
    );
}
Example #6
Source File: StatDisplayComponent.tsx    From genshin-optimizer with MIT License 6 votes vote down vote up
export default function StatDisplayComponent() {
  const { data } = useContext(DataContext)
  const sections = getDisplaySections(data)
  return <Box sx={{ mr: -1, mb: -1 }}>
    <Masonry columns={{ xs: 1, sm: 2, md: 3, xl: 4 }} spacing={1}>
      {sections.map(([key, Nodes]) =>
        <Section key={key} displayNs={Nodes} sectionKey={key} />)}
    </Masonry >
  </Box>
}
Example #7
Source File: UpdateChecker.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 6 votes vote down vote up
function Progress({ progress }: IProgressProps) {
    return (
        <Box sx={{ display: 'grid', placeItems: 'center', position: 'relative' }}>
            <CircularProgress variant="determinate" value={progress} />
            <Box sx={{ position: 'absolute' }}>
                <Typography fontSize="0.8rem">
                    {`${Math.round(progress)}%`}
                </Typography>
            </Box>
        </Box>

    );
}
Example #8
Source File: AdminAppBar.tsx    From frontend with MIT License 6 votes vote down vote up
export function AdminAppBar({ isOpen, children }: Props) {
  return (
    <AppBar position="fixed" open={isOpen} sx={{ p: 0, display: 'flex' }}>
      <Box
        sx={{
          display: 'flex',
          width: 'calc(100% - 24px)',
          position: 'relative',
          paddingRight: '16px',
        }}>
        <Box
          sx={{
            width: drawerWidth,
            height: 64,
            position: 'absolute',
            alignItems: 'center',
            display: 'flex',
            justifyContent: 'space-between',
            padding: '0 19px 0 24px',
          }}>
          <Box sx={{ width: 150, display: 'flex' }}>
            <Link href={routes.admin.index}>
              <Image src={PictureLogo} width={40} height={40} />
            </Link>
          </Box>
        </Box>
        {children}
      </Box>
    </AppBar>
  )
}
Example #9
Source File: Manga.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 6 votes vote down vote up
export default function Manga() {
    const { setTitle } = useContext(NavbarContext);
    useEffect(() => { setTitle('Manga'); }, []); // delegate setting topbar action to MangaDetails

    const { id } = useParams<{ id: string }>();

    const [manga, setManga] = useState<IManga>();

    useEffect(() => {
        if (manga === undefined || !manga.freshData) {
            client.get(`/api/v1/manga/${id}/?onlineFetch=${manga !== undefined}`)
                .then((response) => response.data)
                .then((data: IManga) => {
                    setManga(data);
                    setTitle(data.title);
                });
        }
    }, [manga]);

    return (
        <Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
            <LoadingPlaceholder
                shouldRender={manga !== undefined}
                component={MangaDetails}
                componentProps={{ manga }}
            />

            <ChapterList id={id} />
        </Box>
    );
}
Example #10
Source File: Wallet.tsx    From sapio-studio with Mozilla Public License 2.0 6 votes vote down vote up
export function Wallet(props: { bitcoin_node_manager: BitcoinNodeManager }) {
    const dispatch = useDispatch();
    const idx = useSelector(selectWalletTab);
    const handleChange = (_: any, idx: TabIndexes) => {
        dispatch(switch_wallet_tab(idx));
    };
    return (
        <div className="Wallet">
            <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
                <Tabs
                    value={idx}
                    onChange={handleChange}
                    aria-label="basic tabs example"
                >
                    <Tab label="Send"></Tab>
                    <Tab label="Send History"></Tab>
                    <Tab label="Workspaces"></Tab>
                    <Tab label="Contracts"></Tab>
                </Tabs>
            </Box>
            <Box sx={{ overflowY: 'scroll', height: '100%' }}>
                <WalletSend value={0} idx={idx} {...props}></WalletSend>
                <WalletHistory value={1} idx={idx} {...props}></WalletHistory>
                <Workspaces value={2} idx={idx}></Workspaces>
                <ContractList value={3} idx={idx}></ContractList>
            </Box>
        </div>
    );
}
Example #11
Source File: EmptyView.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 6 votes vote down vote up
export default function EmptyView({ message, messageExtra }: IProps) {
    const theme = useTheme();
    const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm'));

    return (
        <Box sx={{
            position: 'absolute',
            left: `calc(50% + ${isMobileWidth ? '0px' : theme.spacing(8 / 2)})`,
            top: '50%',
            transform: 'translate(-50%, -50%)',
            textAlign: 'center',
        }}
        >
            <Typography variant="h3" gutterBottom>
                {getRandomErrorFace()}
            </Typography>
            <Typography variant="h5">
                {message}
            </Typography>
            {messageExtra}
        </Box>
    );
}
Example #12
Source File: hero.tsx    From usehooks-ts with MIT License 5 votes vote down vote up
Root = styled(Box)(({ theme }) => ({
  backgroundColor: theme.palette.background.default,
  padding: theme.spacing(12, 0, 10),
  display: 'flex',
  flexDirection: 'column',
  justifyContent: 'center',
}))
Example #13
Source File: BuildDisplayItem.tsx    From genshin-optimizer with MIT License 5 votes vote down vote up
//for displaying each artifact build
export default function BuildDisplayItem({ index, compareBuild, extraButtons, disabled }: BuildDisplayItemProps) {
  const { database } = useContext(DatabaseContext)
  const dataContext = useContext(DataContext)

  const { character, data, oldData, mainStatAssumptionLevel } = dataContext
  const artifactSheets = usePromise(ArtifactSheet.getAll, [])
  const [newOld, setNewOld] = useState(undefined as NewOld | undefined)
  const close = useCallback(() => setNewOld(undefined), [setNewOld],)

  const equipBuild = useCallback(() => {
    if (!window.confirm("Do you want to equip this build to this character?")) return
    const newBuild = Object.fromEntries(allSlotKeys.map(s => [s, data.get(input.art[s].id).value])) as Record<SlotKey, string>
    database.equipArtifacts(character.key, newBuild)
    database.setWeaponLocation(data.get(input.weapon.id).value!, character.key)
  }, [character, data, database])
  if (!character || !artifactSheets || !oldData) return null
  const currentlyEquipped = allSlotKeys.every(slotKey => data.get(input.art[slotKey].id).value === oldData.get(input.art[slotKey].id).value) && data.get(input.weapon.id).value === oldData.get(input.weapon.id).value
  const statProviderContext = { ...dataContext }
  if (!compareBuild) statProviderContext.oldData = undefined
  const setToSlots: Partial<Record<ArtifactSetKey, SlotKey[]>> = {}
  allSlotKeys.forEach(slotKey => {
    const set = data.get(input.art[slotKey].set).value as ArtifactSetKey | undefined
    if (!set) return
    if (setToSlots[set]) setToSlots[set]!.push(slotKey)
    else setToSlots[set] = [slotKey]
  })

  return <CardLight>
    <Suspense fallback={<Skeleton variant="rectangular" width="100%" height={600} />}>
      {newOld && <CompareArtifactModal newOld={newOld} mainStatAssumptionLevel={mainStatAssumptionLevel} onClose={close} />}
      <CardContent>
        <Box display="flex" gap={1} sx={{ pb: 1 }} flexWrap="wrap">
          {index !== undefined && <SqBadge color="info"><Typography><strong>#{index + 1}{currentlyEquipped ? " (Equipped)" : ""}</strong></Typography></SqBadge>}
          {(Object.entries(setToSlots) as [ArtifactSetKey, SlotKey[]][]).sort(([k1, slotarr1], [k2, slotarr2]) => slotarr2.length - slotarr1.length).map(([key, slotarr]) =>
            <Box key={key}><SqBadge color={currentlyEquipped ? "success" : "primary"} ><Typography >
              {slotarr.map(slotKey => artifactSlotIcon(slotKey))} {artifactSheets?.[key].name ?? ""}
            </Typography></SqBadge></Box>
          )}
          <Box sx={{ flexGrow: 1, display: "flex", justifyContent: "flex-end" }}>
          </Box>
          <Button size='small' color="success" onClick={equipBuild} disabled={disabled || currentlyEquipped}>Equip Build</Button>
          {extraButtons}
        </Box>
        <Grid container spacing={1} sx={{ pb: 1 }}>
          <Grid item xs={6} sm={4} md={3} lg={2}>
            <WeaponCardNano showLocation weaponId={data.get(input.weapon.id).value} />
          </Grid>
          {allSlotKeys.map(slotKey =>
            <Grid item xs={6} sm={4} md={3} lg={2} key={slotKey} >
              <ArtifactCardNano showLocation slotKey={slotKey} artifactId={data.get(input.art[slotKey].id).value} mainStatAssumptionLevel={mainStatAssumptionLevel} onClick={() => {
                const oldId = character.equippedArtifacts[slotKey]
                const newId = data.get(input.art[slotKey].id).value!
                setNewOld({ oldId: oldId !== newId ? oldId : undefined, newId })
              }} />
            </Grid>)}
        </Grid>
        <DataContext.Provider value={statProviderContext}>
          <StatDisplayComponent />
        </DataContext.Provider>
      </CardContent>
    </Suspense>
  </CardLight>
}
Example #14
Source File: TriStateFilter.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 5 votes vote down vote up
export default function TriStateFilter(props: Props) {
    const {
        state,
        name,
        position,
        group,
        updateFilterValue,
        update,
    } = props;
    const [val, setval] = React.useState({
        [name]: state,
    });

    const handleChange = (checked: boolean | null | undefined) => {
        const tmp = val;
        if (checked !== undefined) {
            tmp[name] = checked ? 1 : 2;
        } else {
            delete tmp[name];
        }
        setval({
            ...tmp,
        });
        const upd = update.filter((e: {
            position: number; group: number | undefined;
        }) => !(position === e.position && group === e.group));
        updateFilterValue([...upd, {
            position,
            state: (tmp[name] === undefined ? 0 : tmp[name]).toString(),
            group,
        }]);
    };

    if (state !== undefined) {
        let check;
        if (val[name] !== 0) {
            check = val[name] === 1;
        } else {
            check = undefined;
        }
        return (
            <Box sx={{ marginLeft: 3 }}>
                <FormControlLabel
                    key={name}
                    control={(
                        <ThreeStateCheckbox name="Unread" checked={check} onChange={(checked) => handleChange(checked)} />
                    )}
                    label={name}
                />
            </Box>
        );
    }
    return (<></>);
}
Example #15
Source File: thanks.tsx    From usehooks-ts with MIT License 5 votes vote down vote up
Thanks = () => {
  const [viewedCount, setViewedCount] = useLocalStorage(
    '1k-stars-viewed-count',
    0,
  )
  const [open, setOpen] = useState(viewedCount < 2)
  const { width, height } = useWindowSize()

  const handleClose = () => setOpen(false)

  useEffect(() => {
    if (open) setViewedCount(viewedCount + 1)
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open])

  if (viewedCount > 1) {
    return null
  }

  return (
    <>
      {open && (
        <Box
          position="absolute"
          left="0"
          top="0"
          zIndex={1500}
          onClick={handleClose}
        >
          <Confetti width={width} height={height} />
        </Box>
      )}
      <Snackbar
        open={open}
        onClose={handleClose}
        anchorOrigin={{
          vertical: 'top',
          horizontal: 'center',
        }}
        ContentProps={{
          message: `1k+ Stargazers on usehooks-ts, thanks! ?`,
          className: classes.content,
        }}
        action={
          <IconButton
            size="small"
            aria-label="close"
            color="inherit"
            onClick={handleClose}
          >
            <CloseIcon fontSize="small" />
          </IconButton>
        }
      />
    </>
  )
}
Example #16
Source File: PersonDialog.tsx    From frontend with MIT License 5 votes vote down vote up
export default function PersonDialog({ label, type, onSubmit }: Props) {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)

  const handleClickOpen = () => setOpen(true)
  const handleClose = () => setOpen(false)

  return (
    <>
      <Button fullWidth variant="contained" color="info" onClick={handleClickOpen}>
        {label}
      </Button>
      <Dialog
        open={open}
        onClose={(e, reason) => {
          if (reason === 'backdropClick') return
          handleClose()
        }}
        onBackdropClick={() => false}>
        <DialogTitle>
          {label}
          <IconButton
            aria-label="close"
            onClick={handleClose}
            sx={{
              position: 'absolute',
              right: 8,
              top: 8,
              color: (theme) => theme.palette.grey[500],
            }}>
            <Close />
          </IconButton>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ mb: 2 }}>
            {type === 'beneficiary' ? (
              <Alert severity="info">
                <AlertTitle>{t('campaigns:campaign.beneficiary.name')}</AlertTitle>
                Лице, в чиято полза се организира кампанията. От юридическа гледна точка,
                бенефициентът <strong>НЕ влиза</strong> във взаимоотношения с оператора при набиране
                на средства в негова полза. Всички договори, изисквания, банкова сметка на
                кампанията са на името на организатора. Възможно е бенефициентът по една кампания да
                е и неговият организатор.
              </Alert>
            ) : (
              <Alert severity="warning">
                <AlertTitle>{t('campaigns:campaign.coordinator.name')}</AlertTitle>
                Организаторът е физическото или юридическо лице, с което се сключва договор за
                набиране на средства, след като негова заявка за кампания е одобрена. Набраните
                средства се прехвърлят в неговата банкова сметка, от него се изискват отчети за
                разходените средства. Когато дадено лице иска да стане организатор на кампании,
                преминава през процес на верификация, за да се избегнат измамите. Организаторът също
                може да е и бенефициент по дадена кампания.
              </Alert>
            )}
          </Box>
          <PersonForm
            {...type}
            onSubmit={(...args) => {
              onSubmit(...args)
              handleClose()
            }}
          />
        </DialogContent>
      </Dialog>
    </>
  )
}
Example #17
Source File: PriceGranularityComponent.tsx    From professor-prebid with Apache License 2.0 5 votes vote down vote up
PriceGranularityComponent = ({ priceGranularity, customPriceBucket }: IPriceGranularityComponentProps) => {
  const [type, setType] = React.useState<string>();
  const [rows, setRows] = React.useState<IPrebidConfigPriceBucket[]>([]);
  useEffect(() => {
    const type = priceGranularity;
    setType(type);
    const rows = defaultBuckets[type] || customPriceBucket?.buckets || [];
    setRows(rows);
  }, [priceGranularity, customPriceBucket?.buckets]);

  logger.log(`[PopUp][PriceGranularityComponent]: render `, type, rows);
  return (
    <Box sx={{ backgroundColor: 'text.disabled', p: 0.25, borderRadius: 1 }}>
      <Grid container spacing={0.2}>
        <Grid item xs={3}>
          <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
            <Typography variant="h3">Bucket</Typography>
          </Paper>
        </Grid>
        <Grid item xs={3}>
          <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
            <Typography variant="h3">Precision</Typography>
          </Paper>
        </Grid>
        <Grid item xs={2}>
          <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
            <Typography variant="h3">Min</Typography>
          </Paper>
        </Grid>
        <Grid item xs={2}>
          <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
            <Typography variant="h3">Max</Typography>
          </Paper>
        </Grid>
        <Grid item xs={2}>
          <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
            <Typography variant="h3">Increment</Typography>
          </Paper>
        </Grid>

        {rows.map((row, index) => {
          return (
            <React.Fragment key={index}>
              <Grid xs={3} item>
                <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
                  <Typography variant="body1">
                    {type} #{index + 1}
                  </Typography>
                </Paper>
              </Grid>
              <Grid xs={3} item>
                <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
                  <Typography variant="body1">{row.precision || 2}</Typography>
                </Paper>
              </Grid>
              <Grid xs={2} item>
                <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
                  <Typography variant="body1">{row.min}</Typography>
                </Paper>
              </Grid>
              <Grid xs={2} item>
                <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
                  <Typography variant="body1">{row.max}</Typography>
                </Paper>
              </Grid>
              <Grid xs={2} item>
                <Paper sx={{ height: 1, borderRadius: 1, display: 'flex', justifyContent: 'center' }}>
                  <Typography variant="body1">{row.increment}</Typography>
                </Paper>
              </Grid>
            </React.Fragment>
          );
        })}
      </Grid>
    </Box>
  );
}
Example #18
Source File: WalletSendForm.tsx    From sapio-studio with Mozilla Public License 2.0 5 votes vote down vote up
export function WalletSendForm(props: {
    bitcoin_node_manager: BitcoinNodeManager;
    set_params: (a: number, b: string) => void;
}) {
    const [address, setAddress] = React.useState<string | null>(null);

    const get_address = async () => {
        try {
            const address = await props.bitcoin_node_manager.get_new_address();
            setAddress(address);
        } catch (err) {
            // console.error(err);
            setAddress(null);
        }
    };
    const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
        event
    ) => {
        event.preventDefault();
        const amt = event.currentTarget.amount.value;
        const to = event.currentTarget.address.value;
        props.set_params(amt, to);
        event.currentTarget.reset();
    };

    return (
        <div className="WalletSpendInner">
            <div></div>
            <div>
                <AvailableBalance
                    bitcoin_node_manager={props.bitcoin_node_manager}
                />
                <Typography>{address && `New Address: ${address}`}</Typography>
                <Button onClick={() => get_address()}>Get Address</Button>
                <Box
                    component="form"
                    noValidate
                    autoComplete="off"
                    onSubmit={handleSubmit}
                >
                    <TextField
                        label="Address"
                        name="address"
                        type="text"
                        required={true}
                        size="small"
                    />
                    <TextField
                        label="Amount"
                        name="amount"
                        type="number"
                        required={true}
                        size="small"
                    />
                    <Button type="submit">Send</Button>
                </Box>
            </div>
            <div></div>
        </div>
    );
}
Example #19
Source File: SelectFilter.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 5 votes vote down vote up
function hasSelect(
    values: Selected[],
    name: string,
    state: number,
    position: number,
    updateFilterValue: Function,
    update: any,
    group?: number,
) {
    const [val, setval] = React.useState(state);
    if (values) {
        const handleChange = (event: { target: { name: any; value: any; }; }) => {
            const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`);
            setval(vall);
            const upd = update.filter((e: {
                position: number; group: number | undefined;
            }) => !(position === e.position && group === e.group));
            updateFilterValue([...upd, { position, state: vall.toString(), group }]);
        };

        const rett = values.map((e: Selected) => (
            <MenuItem
                key={`${name} ${e.displayname}`}
                value={e.displayname}
            >
                {
                    e.displayname
                }
            </MenuItem>
        ));
        return (
            <Box key={name} sx={{ display: 'flex', flexDirection: 'column', minWidth: 120 }}>
                <FormControl fullWidth>
                    <InputLabel sx={{ margin: '10px 0 10px 0' }}>
                        {name}
                    </InputLabel>
                    <Select
                        name={name}
                        value={values[val].displayname}
                        label={name}
                        onChange={handleChange}
                        autoWidth
                        sx={{ margin: '10px 0 10px 0' }}
                    >
                        {rett}
                    </Select>
                </FormControl>
            </Box>
        );
    }
    return (<></>);
}
Example #20
Source File: GamDetailsComponent.tsx    From professor-prebid with Apache License 2.0 4 votes vote down vote up
GamDetailsComponent = ({ elementId, inPopOver }: IGamDetailComponentProps): JSX.Element => {
  const [networktId, setNetworkId] = useState<string>(null);
  const [slotElementId, setSlotElementId] = useState<string>(null);
  const [creativeId, setCreativeId] = useState<number>(null);
  const [lineItemId, setLineItemId] = useState<number>(null);
  const [slotAdUnitPath, setSlotAdUnitPath] = useState<string>(null);
  const [slotTargeting, setSlotTargeting] = useState<{ key: string; value: string; id: number }[]>(null);
  const [slotResponseInfo, setSlotResponseInfo] = useState<googletag.ResponseInformation>(null);

  useEffect(() => {
    if (googletag && typeof googletag?.pubads === 'function') {
      const pubads = googletag.pubads();
      const slots = pubads.getSlots();
      const slot = slots.find((slot) => slot.getSlotElementId() === elementId) || slots.find((slot) => slot.getAdUnitPath() === elementId);
      if (slot) {
        setSlotElementId(slot.getSlotElementId());
        setSlotAdUnitPath(slot.getAdUnitPath());
        setNetworkId(slot.getAdUnitPath()?.split('/')[1]);
        setSlotTargeting((slot as any).getTargetingKeys().map((key: string, id: number) => ({ key, value: slot.getTargeting(key), id })));
        setSlotResponseInfo(slot.getResponseInformation());
        if (slotResponseInfo) {
          const { creativeId, lineItemId, sourceAgnosticCreativeId, sourceAgnosticLineItemId } = slotResponseInfo as any;
          setCreativeId(creativeId || sourceAgnosticCreativeId);
          setLineItemId(lineItemId || sourceAgnosticLineItemId);
        }
        const eventHandler = (event: googletag.events.SlotRenderEndedEvent | googletag.events.SlotResponseReceived) => {
          if (event.slot.getSlotElementId() === slot.getSlotElementId()) {
            setSlotResponseInfo(slot.getResponseInformation());
          }
        };
        pubads.addEventListener('slotResponseReceived', eventHandler);
        pubads.addEventListener('slotRenderEnded', eventHandler);
        return () => {
          pubads.removeEventListener('slotResponseReceived', eventHandler);
          pubads.removeEventListener('slotRenderEnded', eventHandler);
        };
      }
    }
  }, [elementId, inPopOver, slotResponseInfo]);

  return (
    <React.Fragment>
      {lineItemId && (
        <Grid item>
          <Paper elevation={1} sx={{ p: inPopOver ? 1 : 0.5 }}>
            <Typography component={'span'} variant="h4">
              LineItem-ID:{' '}
            </Typography>
            <Typography component={'span'} variant="body1" sx={{ '& a': { color: 'secondary.main' } }}>
              <a
                href={`https://admanager.google.com/${networktId}#delivery/LineItemDetail/lineItemId=${lineItemId}`}
                rel="noreferrer"
                target="_blank"
              >
                {lineItemId}
              </a>
            </Typography>
          </Paper>
        </Grid>
      )}

      {creativeId && (
        <Grid item>
          <Paper elevation={1} sx={{ p: inPopOver ? 1 : 0.5 }}>
            <Typography variant="h4" component={'span'}>
              Creative-ID:{' '}
            </Typography>
            <Typography component={'span'} variant="body1" sx={{ '& a': { color: 'secondary.main' } }}>
              <a
                href={`https://admanager.google.com/${networktId}#delivery/CreativeDetail/creativeId=${creativeId}`}
                rel="noreferrer"
                target="_blank"
              >
                {creativeId}
              </a>
            </Typography>
          </Paper>
        </Grid>
      )}

      {slotAdUnitPath && (
        <Grid item>
          <Paper elevation={1} sx={{ p: inPopOver ? 1 : 0.5 }}>
            <Typography variant="h4" component={'span'}>
              AdUnit Path:{' '}
            </Typography>
            <Typography variant="body1" component={'span'}>
              {slotAdUnitPath}
            </Typography>
          </Paper>
        </Grid>
      )}

      {slotElementId && (
        <Grid item>
          <Paper elevation={1} sx={{ p: inPopOver ? 1 : 0.5 }}>
            <Typography variant="h4" component={'span'}>
              Element-ID:{' '}
            </Typography>
            <Typography variant="body1" component={'span'}>
              {slotElementId}
            </Typography>
          </Paper>
        </Grid>
      )}

      {inPopOver && (
        <React.Fragment>
          <Grid item xs={12}>
            <Grid container direction={'column'} spacing={1}>
              {slotResponseInfo && (
                <Grid item>
                  <Paper elevation={1} sx={{ p: inPopOver ? 1 : 0.5 }}>
                    <Typography sx={{ fontWeight: 'bold' }}>Response-Info:</Typography>
                    <ReactJson
                      name={false}
                      src={slotResponseInfo}
                      collapsed={false}
                      enableClipboard={false}
                      displayObjectSize={true}
                      displayDataTypes={false}
                      sortKeys={false}
                      quotesOnKeys={false}
                      indentWidth={2}
                      collapseStringsAfterLength={100}
                      style={{
                        fontSize: '12px',
                        fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
                        fontWeight: 400,
                        lineHeight: 1.5,
                        letterSpacing: '0.00938em',
                        borderRadius: '4px',
                      }}
                    />
                  </Paper>
                </Grid>
              )}

              {slotTargeting && (
                <Grid item xs={12}>
                  <Paper elevation={1} sx={{ p: 1 }}>
                    <Typography sx={{ fontWeight: 'bold' }}>Targeting:</Typography>
                    <Box sx={{ display: 'flex', flexGrow: 1 }}>
                      <DataGrid
                        density="compact"
                        rows={slotTargeting}
                        columns={[
                          { field: 'key', headerName: 'Key', align: 'left', width: 200 },
                          { field: 'value', headerName: 'Value', align: 'left', width: 200 },
                        ]}
                        disableSelectionOnClick
                        autoHeight
                        hideFooter
                      />
                    </Box>
                  </Paper>
                </Grid>
              )}
            </Grid>
          </Grid>
        </React.Fragment>
      )}
    </React.Fragment>
  );
}
Example #21
Source File: ChapterCard.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function ChapterCard(props: IProps) {
    const theme = useTheme();

    const {
        chapter, triggerChaptersUpdate, downloadStatusString, showChapterNumber,
    } = props;

    const dateStr = chapter.uploadDate && new Date(chapter.uploadDate).toLocaleDateString();

    const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);

    const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
        // prevent parent tags from getting the event
        event.stopPropagation();
        event.preventDefault();

        setAnchorEl(event.currentTarget);
    };

    const handleClose = () => {
        setAnchorEl(null);
    };

    const sendChange = (key: string, value: any) => {
        handleClose();

        const formData = new FormData();
        formData.append(key, value);
        if (key === 'read') { formData.append('lastPageRead', '1'); }
        client.patch(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`, formData)
            .then(() => triggerChaptersUpdate());
    };

    const downloadChapter = () => {
        client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
        handleClose();
    };

    const deleteChapter = () => {
        client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`)
            .then(() => triggerChaptersUpdate());

        handleClose();
    };

    const readChapterColor = theme.palette.mode === 'dark' ? '#acacac' : '#b0b0b0';

    return (
        <>
            <li>
                <Card
                    sx={{
                        margin: '10px',
                        ':hover': {
                            backgroundColor: 'action.hover',
                            transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                            cursor: 'pointer',
                        },
                        ':active': {
                            backgroundColor: 'action.selected',
                            transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                        },
                    }}
                >
                    <Link
                        to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
                        style={{
                            textDecoration: 'none',
                            color: chapter.read ? readChapterColor : theme.palette.text.primary,
                        }}
                    >
                        <CardContent
                            sx={{
                                display: 'flex',
                                justifyContent: 'space-between',
                                alignItems: 'center',
                                padding: 2,
                            }}
                        >
                            <Box sx={{ display: 'flex' }}>
                                <div style={{ display: 'flex', flexDirection: 'column' }}>
                                    <Typography variant="h5" component="h2">
                                        <span style={{ color: theme.palette.primary.dark }}>
                                            {chapter.bookmarked && <BookmarkIcon />}
                                        </span>
                                        { showChapterNumber ? `Chapter ${chapter.chapterNumber}` : chapter.name}
                                    </Typography>
                                    <Typography variant="caption" display="block" gutterBottom>
                                        {chapter.scanlator}
                                        {chapter.scanlator && ' '}
                                        {dateStr}
                                        {downloadStatusString}
                                    </Typography>
                                </div>
                            </Box>

                            <IconButton aria-label="more" onClick={handleClick} size="large">
                                <MoreVertIcon />
                            </IconButton>
                        </CardContent>
                    </Link>
                    <Menu
                        anchorEl={anchorEl}
                        keepMounted
                        open={Boolean(anchorEl)}
                        onClose={handleClose}
                    >
                        {downloadStatusString.endsWith('Downloaded')
                             && <MenuItem onClick={deleteChapter}>Delete</MenuItem>}
                        {downloadStatusString.length === 0
                         && <MenuItem onClick={downloadChapter}>Download</MenuItem> }
                        <MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
                            {chapter.bookmarked && 'Remove bookmark'}
                            {!chapter.bookmarked && 'Bookmark'}
                        </MenuItem>
                        <MenuItem onClick={() => sendChange('read', !chapter.read)}>
                            {`Mark as ${chapter.read ? 'unread' : 'read'}`}
                        </MenuItem>
                        <MenuItem onClick={() => sendChange('markPrevRead', true)}>
                            Mark previous as Read
                        </MenuItem>
                    </Menu>
                </Card>
            </li>
        </>
    );
}
Example #22
Source File: DonationTable.tsx    From frontend with MIT License 4 votes vote down vote up
function DonationTable({ donations }: DonationTableProps) {
  const { t, i18n } = useTranslation()
  const [fromDate, setFromDate] = React.useState<Date | null>(null)
  const [toDate, setToDate] = React.useState<Date | null>(null)
  const [monthly, setMonthly] = React.useState(true)
  const [oneTime, setOneTime] = React.useState(true)
  const filteredByTypeDonations = useMemo(() => {
    if (monthly && oneTime) {
      return donations
    }
    if (!monthly && !oneTime) {
      return []
    }
    if (monthly) {
      return donations?.filter((d) => d.type !== 'donation')
    }
    if (oneTime) {
      return donations?.filter((d) => d.type === 'donation')
    }
    return donations
  }, [donations, monthly, oneTime])
  const filteredDonations = useMemo(() => {
    if (!fromDate && !toDate) {
      return filteredByTypeDonations
    }
    if (fromDate && toDate) {
      return filteredByTypeDonations?.filter((d) => {
        const createdAtDate = parseISO(d.createdAt)
        return isAfter(createdAtDate, fromDate) && isBefore(createdAtDate, toDate)
      })
    }
    if (fromDate) {
      return filteredByTypeDonations?.filter((d) => {
        const createdAtDate = parseISO(d.createdAt)
        return isAfter(createdAtDate, fromDate)
      })
    }
    if (toDate) {
      return filteredByTypeDonations?.filter((d) => {
        const createdAtDate = parseISO(d.createdAt)
        return isBefore(createdAtDate, toDate)
      })
    }
  }, [filteredByTypeDonations, fromDate, toDate])
  return (
    <Card sx={{ padding: theme.spacing(2) }}>
      <Grid container alignItems={'flex-start'} spacing={theme.spacing(2)}>
        <Grid item xs={6} sm={3}>
          <CheckboxLabel>{t('profile:donations.oneTime')}</CheckboxLabel>
          <Checkbox
            onChange={(e, checked) => setOneTime(checked)}
            checked={oneTime}
            name="oneTime"
          />
        </Grid>
        <Grid item xs={6} sm={3}>
          <CheckboxLabel>{t('profile:donations.monthly')}</CheckboxLabel>
          <Checkbox
            onChange={(e, checked) => setMonthly(checked)}
            checked={monthly}
            name="monthly"
          />
        </Grid>
        <LocalizationProvider
          locale={i18n.language === 'bg' ? bg : enUS}
          dateAdapter={AdapterDateFns}>
          <Grid item xs={12} sm={3}>
            <DatePicker
              label={t('profile:donations.fromDate')}
              value={fromDate}
              onChange={setFromDate}
              renderInput={(params) => <TextField size="small" {...params} />}
            />
          </Grid>
          <Grid item xs={12} sm={3}>
            <DatePicker
              label={t('profile:donations.toDate')}
              value={toDate}
              onChange={setToDate}
              renderInput={(params) => <TextField size="small" {...params} />}
            />
          </Grid>
        </LocalizationProvider>
      </Grid>
      {filteredDonations?.length ? (
        <TableContainer>
          <Table sx={{ minWidth: 650, backgroundColor: 'white' }} aria-label="simple table">
            <TableHead>
              <TableRow>
                <TableCell>№</TableCell>
                <TableCell>{t('profile:donations.date')}</TableCell>
                <TableCell>{t('profile:donations.type')}</TableCell>
                <TableCell>{t('profile:donations.cause')}</TableCell>
                <TableCell>{t('profile:donations.amount')}</TableCell>
                <TableCell>{t('profile:donations.certificate')}</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {filteredDonations.map((donation, index) => (
                <TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
                  <TableCell component="th" scope="row">
                    {index + 1}
                  </TableCell>
                  <TableCell>
                    {format(parseISO(donation.createdAt), 'd.LL.yyyy', {
                      locale: i18n.language === 'bg' ? bg : enUS,
                    })}
                  </TableCell>
                  <TableCell>
                    <Avatar sx={{ background: darken(theme.palette.secondary.main, 0.175) }}>
                      <StarIcon />
                    </Avatar>
                  </TableCell>
                  <TableCell>{donation.targetVault.campaign.title}</TableCell>
                  <TableCell>{money(donation.amount)}</TableCell>
                  <TableCell>
                    <Button variant="outlined" disabled={donation.status != 'succeeded'}>
                      <Link target="_blank" href={routes.donation.viewCertificate(donation.id)}>
                        {t('profile:donations.download')} <ArrowForwardIcon />
                      </Link>
                    </Button>
                  </TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </TableContainer>
      ) : (
        <Box sx={{ fontSize: 20, mt: 4 }}>Към момента няма направени дарения</Box>
      )}
    </Card>
  )
}
Example #23
Source File: ChapterList.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function ChapterList(props: IProps) {
    const { id } = props;

    const [chapters, triggerChaptersUpdate, noChaptersFound] = useFetchChapters(id);
    const [firstUnreadChapter, setFirstUnreadChapter] = useState<IChapter>();
    const [filteredChapters, setFilteredChapters] = useState<IChapter[]>([]);
    // eslint-disable-next-line max-len
    const [options, optionsDispatch] = useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
        chapterOptionsReducer, `${id}filterOptions`, defaultChapterOptions,
    );

    const [, setWsClient] = useState<WebSocket>();
    const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);

    useEffect(() => {
        const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
        wsc.onmessage = (e) => {
            const data = JSON.parse(e.data) as IQueue;
            setQueueState(data);
        };

        setWsClient(wsc);

        return () => wsc.close();
    }, []);

    useEffect(() => {
        triggerChaptersUpdate();
    }, [queue.length]);

    const downloadStatusStringFor = useCallback((chapter: IChapter) => {
        let rtn = '';
        if (chapter.downloaded) {
            rtn = ' • Downloaded';
        }
        queue.forEach((q) => {
            if (chapter.index === q.chapterIndex && chapter.mangaId === q.mangaId) {
                rtn = ` • Downloading (${(q.progress * 100).toFixed(2)}%)`;
            }
        });
        return rtn;
    }, [queue]);

    useEffect(() => {
        const filtered = filterAndSortChapters(chapters, options);
        setFilteredChapters(filtered);
        setFirstUnreadChapter(findFirstUnreadChapter(filtered));
    }, [options, chapters]);

    useEffect(() => {
        if (noChaptersFound) {
            makeToast('No chapters found', 'warning');
        }
    }, [noChaptersFound]);

    if (chapters.length === 0 || noChaptersFound) {
        return (
            <div style={{
                margin: '10px auto',
                display: 'flex',
                justifyContent: 'center',
            }}
            >
                <CircularProgress thickness={5} />
            </div>
        );
    }

    return (
        <>
            <Stack direction="column">
                <Box sx={{
                    display: 'flex', justifyContent: 'space-between', px: 1.5, mt: 1,
                }}
                >
                    <Typography variant="h5">
                        {`${filteredChapters.length} Chapters`}
                    </Typography>
                    <ChapterOptions options={options} optionsDispatch={optionsDispatch} />
                </Box>

                <CustomVirtuoso
                    style={{ // override Virtuoso default values and set them with class
                        height: 'undefined',
                        // 900 is the md breakpoint in MUI
                        overflowY: window.innerWidth < 900 ? 'visible' : 'auto',
                    }}
                    totalCount={filteredChapters.length}
                    itemContent={(index:number) => (
                        <ChapterCard
                            showChapterNumber={options.showChapterNumber}
                            chapter={filteredChapters[index]}
                            downloadStatusString={downloadStatusStringFor(filteredChapters[index])}
                            triggerChaptersUpdate={triggerChaptersUpdate}
                        />
                    )}
                    useWindowScroll={window.innerWidth < 900}
                    overscan={window.innerHeight * 0.5}
                />
            </Stack>
            {firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={id} />}
        </>
    );
}
Example #24
Source File: WidgetsSidebar.tsx    From fluttertemplates.dev with MIT License 4 votes vote down vote up
function SingleSubGroupRenderer(props: SingleSubGroupRendererProps) {
  const theme = useTheme();

  const [expanded, setExpanded] = useState(false);
  const [isSelected, setIsSelected] = useState(false);

  const handleChange = () => {
    setExpanded(!expanded);
  };
  useEffect(() => {
    setExpanded(props.selectedSubGroup?.id === props.subgroup.id);
    setIsSelected(props.selectedSubGroup?.id === props.subgroup.id);
  }, [props.selectedSubGroup]);

  return (
    <>
      <Grid
        container
        style={{
          borderRadius: `0 1rem 1rem 0`,
          backgroundColor: isSelected
            ? `${theme.palette.secondary.main}10`
            : "",
        }}
      >
        <Box sx={{ flexGrow: 1 }}>
          <Link
            href={`/widgets/${props.subgroup.id}`}
            replace
            key={`sub_group_${props.subgroup.id}`}
          >
            <a>
              <ListItem
                button
                style={{
                  borderRadius: "0 1rem 1rem 0",
                }}
              >
                <Typography
                  variant="body2"
                  style={{
                    color: isSelected ? theme.palette.secondary.main : "",
                  }}
                >
                  {props.subgroup.title}
                </Typography>
              </ListItem>
            </a>
          </Link>
        </Box>
        <Grid item>
          <IconButton onClick={() => handleChange()}>
            {expanded ? <ExpandLess /> : <ExpandMore />}
          </IconButton>
        </Grid>
      </Grid>

      <AnimatePresence initial={false}>
        {expanded && (
          <motion.section
            key="content"
            initial="collapsed"
            animate="open"
            exit="collapsed"
            variants={{
              open: { opacity: 1, height: "auto" },
              collapsed: { opacity: 0, height: 0 },
            }}
          >
            <div
              style={{
                marginLeft: "1rem",
                borderRadius: "0 0 1rem 1rem",
              }}
            >
              {props.subgroup.widgets.map((widget, index) => (
                <Link
                  href={
                    isSelected
                      ? `#${widget.id.split("/").slice(-1)[0]}`
                      : `/widgets/${props.subgroup.id}#${
                          widget.id.split("/").slice(-1)[0]
                        }`
                  }
                  replace={!isSelected}
                  scroll={true}
                  key={`sub_group_${props.subgroup.id}_widget_${index}`}
                >
                  <a>
                    <ListItem
                      button
                      style={{
                        borderRadius: "1rem",
                      }}
                    >
                      <Typography variant="body2">
                        {index + 1}. {widget.title}
                      </Typography>
                    </ListItem>
                  </a>
                </Link>
              ))}
            </div>
          </motion.section>
        )}
      </AnimatePresence>
    </>
  );
}
Example #25
Source File: Updates.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function Updates() {
    const history = useHistory();

    const { setTitle, setAction } = useContext(NavbarContext);
    const [updateEntries, setUpdateEntries] = useState<IMangaChapter[]>([]);
    const [hasNextPage, setHasNextPage] = useState(true);
    const [fetched, setFetched] = useState(false);
    const [lastPageNum, setLastPageNum] = useState(0);

    const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
    const [useCache] = useLocalStorage<boolean>('useCache', true);

    const [, setWsClient] = useState<WebSocket>();
    const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);

    useEffect(() => {
        const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
        wsc.onmessage = (e) => {
            const data = JSON.parse(e.data) as IQueue;
            setQueueState(data);
        };

        setWsClient(wsc);

        return () => wsc.close();
    }, []);

    useEffect(() => {
        setTitle('Updates');

        setAction(<></>);
    }, []);

    useEffect(() => {
        if (hasNextPage) {
            client.get(`/api/v1/update/recentChapters/${lastPageNum}`)
                .then((response) => response.data)
                .then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => {
                    setUpdateEntries([
                        ...updateEntries,
                        ...page,
                    ]);
                    setHasNextPage(fetchedHasNextPage);
                    setFetched(true);
                });
        }
    }, [lastPageNum]);

    const lastEntry = useRef<HTMLDivElement>(null);

    const scrollHandler = () => {
        if (lastEntry.current) {
            const rect = lastEntry.current.getBoundingClientRect();
            if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
                setLastPageNum(lastPageNum + 1);
            }
        }
    };
    useEffect(() => {
        window.addEventListener('scroll', scrollHandler, true);
        return () => {
            window.removeEventListener('scroll', scrollHandler, true);
        };
    }, [hasNextPage, updateEntries]);

    if (!fetched) { return <LoadingPlaceholder />; }
    if (fetched && updateEntries.length === 0) { return <EmptyView message="You don't have any updates yet." />; }

    const downloadStatusStringFor = (chapter: IChapter) => {
        let rtn = '';
        if (chapter.downloaded) {
            rtn = ' • Downloaded';
        }
        queue.forEach((q) => {
            if (chapter.index === q.chapterIndex && chapter.mangaId === q.mangaId) {
                rtn = ` • Downloading (${(q.progress * 100).toFixed(2)}%)`;
            }
        });
        return rtn;
    };

    const downloadChapter = (chapter: IChapter) => {
        client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
    };

    return (
        <>
            {groupByDate(updateEntries).map((dateGroup) => (
                <div key={dateGroup[0]}>
                    <Typography
                        variant="h5"
                        sx={{
                            ml: 3,
                            my: 2,
                            fontWeight: 700,
                        }}
                    >
                        {dateGroup[0]}
                    </Typography>
                    {dateGroup[1].map(({ item: { chapter, manga }, globalIdx }) => (
                        <Card
                            ref={globalIdx === updateEntries.length - 1 ? lastEntry : undefined}
                            key={globalIdx}
                            sx={{
                                margin: '10px',
                                '&:hover': {
                                    backgroundColor: 'action.hover',
                                    transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                                },
                                '&:active': {
                                    backgroundColor: 'action.selected',
                                    transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                                },
                            }}
                            onClick={() => history.push(`/manga/${chapter.mangaId}/chapter/${chapter.index}`)}
                        >
                            <CardContent sx={{
                                display: 'flex',
                                justifyContent: 'space-between',
                                alignItems: 'center',
                                padding: 2,
                            }}
                            >
                                <Box sx={{ display: 'flex' }}>
                                    <Avatar
                                        variant="rounded"
                                        sx={{
                                            width: 56,
                                            height: 56,
                                            flex: '0 0 auto',
                                            marginRight: 2,
                                            imageRendering: 'pixelated',
                                        }}
                                        src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`}
                                    />
                                    <Box sx={{ display: 'flex', flexDirection: 'column' }}>
                                        <Typography variant="h5" component="h2">
                                            {manga.title}
                                        </Typography>
                                        <Typography variant="caption" display="block" gutterBottom>
                                            {chapter.name}
                                            {downloadStatusStringFor(chapter)}
                                        </Typography>
                                    </Box>
                                </Box>
                                {downloadStatusStringFor(chapter) === ''
                                        && (
                                            <IconButton
                                                onClick={(e) => {
                                                    downloadChapter(chapter);
                                                    // prevent parent tags from getting the event
                                                    e.stopPropagation();
                                                }}
                                                size="large"
                                            >
                                                <DownloadIcon />
                                            </IconButton>
                                        )}
                            </CardContent>
                        </Card>
                    ))}
                </div>
            ))}
        </>
    );
}