JS constants

This commit is contained in:
binwiederhier 2023-01-30 13:10:45 -05:00
parent ef8f7c9884
commit 259293f9b3
8 changed files with 77 additions and 44 deletions

View file

@ -43,7 +43,6 @@ import (
- MEDIUM: Reservation (UI): Ask for confirmation when removing reservation (deadcade) - MEDIUM: Reservation (UI): Ask for confirmation when removing reservation (deadcade)
- MEDIUM: Reservation table delete button: dialog "keep or delete messages?" - MEDIUM: Reservation table delete button: dialog "keep or delete messages?"
- LOW: UI: Flickering upgrade banner when logging in - LOW: UI: Flickering upgrade banner when logging in
- LOW: JS constants
*/ */

View file

@ -1,21 +1,23 @@
import { import {
accountBillingPortalUrl,
accountBillingSubscriptionUrl,
accountPasswordUrl,
accountReservationSingleUrl, accountReservationSingleUrl,
accountReservationUrl, accountReservationUrl,
accountPasswordUrl,
accountSettingsUrl, accountSettingsUrl,
accountSubscriptionSingleUrl, accountSubscriptionSingleUrl,
accountSubscriptionUrl, accountSubscriptionUrl,
accountTokenUrl, accountTokenUrl,
accountUrl, maybeWithAuth, topicUrl, accountUrl,
tiersUrl,
withBasicAuth, withBasicAuth,
withBearerAuth, accountBillingSubscriptionUrl, accountBillingPortalUrl, tiersUrl withBearerAuth
} from "./utils"; } from "./utils";
import session from "./Session"; import session from "./Session";
import subscriptionManager from "./SubscriptionManager"; import subscriptionManager from "./SubscriptionManager";
import i18n from "i18next"; import i18n from "i18next";
import prefs from "./Prefs"; import prefs from "./Prefs";
import routes from "../components/routes"; import routes from "../components/routes";
import userManager from "./UserManager";
const delayMillis = 45000; // 45 seconds const delayMillis = 45000; // 45 seconds
const intervalMillis = 900000; // 15 minutes const intervalMillis = 900000; // 15 minutes
@ -441,6 +443,32 @@ class AccountApi {
} }
} }
// Maps to user.Role in user/types.go
export const Role = {
ADMIN: "admin",
USER: "user"
};
// Maps to server.visitorLimitBasis in server/visitor.go
export const LimitBasis = {
IP: "ip",
TIER: "tier"
};
// Maps to stripe.SubscriptionStatus
export const SubscriptionStatus = {
ACTIVE: "active",
PAST_DUE: "past_due"
};
// Maps to user.Permission in user/types.go
export const Permission = {
READ_WRITE: "read-write",
READ_ONLY: "read-only",
WRITE_ONLY: "write-only",
DENY_ALL: "deny-all"
};
export class UsernameTakenError extends Error { export class UsernameTakenError extends Error {
constructor(username) { constructor(username) {
super("Username taken"); super("Username taken");

View file

@ -28,7 +28,13 @@ import TextField from "@mui/material/TextField";
import routes from "./routes"; import routes from "./routes";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import {formatBytes, formatShortDate, formatShortDateTime, openUrl, truncateString, validUrl} from "../app/utils"; import {formatBytes, formatShortDate, formatShortDateTime, openUrl, truncateString, validUrl} from "../app/utils";
import accountApi, {IncorrectPasswordError, UnauthorizedError} from "../app/AccountApi"; import accountApi, {
IncorrectPasswordError,
LimitBasis,
Role,
SubscriptionStatus,
UnauthorizedError
} from "../app/AccountApi";
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import {Pref, PrefGroup} from "./Pref"; import {Pref, PrefGroup} from "./Pref";
import db from "../app/db"; import db from "../app/db";
@ -92,7 +98,7 @@ const Username = () => {
<Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}> <Pref labelId={labelId} title={t("account_basics_username_title")} description={t("account_basics_username_description")}>
<div aria-labelledby={labelId}> <div aria-labelledby={labelId}>
{session.username()} {session.username()}
{account?.role === "admin" {account?.role === Role.ADMIN
? <>{" "}<Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></> ? <>{" "}<Tooltip title={t("account_basics_username_admin_tooltip")}><span style={{cursor: "default"}}>👑</span></Tooltip></>
: ""} : ""}
</div> </div>
@ -237,7 +243,7 @@ const AccountType = () => {
}; };
let accountType; let accountType;
if (account.role === "admin") { if (account.role === Role.ADMIN) {
const tierSuffix = (account.tier) ? `(with ${account.tier.name} tier)` : `(no tier)`; const tierSuffix = (account.tier) ? `(with ${account.tier.name} tier)` : `(no tier)`;
accountType = `${t("account_usage_tier_admin")} ${tierSuffix}`; accountType = `${t("account_usage_tier_admin")} ${tierSuffix}`;
} else if (!account.tier) { } else if (!account.tier) {
@ -248,7 +254,7 @@ const AccountType = () => {
return ( return (
<Pref <Pref
alignTop={account.billing?.status === "past_due" || account.billing?.cancel_at > 0} alignTop={account.billing?.status === SubscriptionStatus.PAST_DUE || account.billing?.cancel_at > 0}
title={t("account_usage_tier_title")} title={t("account_usage_tier_title")}
description={t("account_usage_tier_description")} description={t("account_usage_tier_description")}
> >
@ -259,7 +265,7 @@ const AccountType = () => {
<span><InfoIcon/></span> <span><InfoIcon/></span>
</Tooltip> </Tooltip>
} }
{config.enable_payments && account.role === "user" && !account.billing?.subscription && {config.enable_payments && account.role === Role.USER && !account.billing?.subscription &&
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
@ -268,7 +274,7 @@ const AccountType = () => {
sx={{ml: 1}} sx={{ml: 1}}
>{t("account_usage_tier_upgrade_button")}</Button> >{t("account_usage_tier_upgrade_button")}</Button>
} }
{config.enable_payments && account.role === "user" && account.billing?.subscription && {config.enable_payments && account.role === Role.USER && account.billing?.subscription &&
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
@ -276,7 +282,7 @@ const AccountType = () => {
sx={{ml: 1}} sx={{ml: 1}}
>{t("account_usage_tier_change_button")}</Button> >{t("account_usage_tier_change_button")}</Button>
} }
{config.enable_payments && account.role === "user" && account.billing?.customer && {config.enable_payments && account.role === Role.USER && account.billing?.customer &&
<Button <Button
variant="outlined" variant="outlined"
size="small" size="small"
@ -290,7 +296,7 @@ const AccountType = () => {
onCancel={() => setUpgradeDialogOpen(false)} onCancel={() => setUpgradeDialogOpen(false)}
/> />
</div> </div>
{account.billing?.status === "past_due" && {account.billing?.status === SubscriptionStatus.PAST_DUE &&
<Alert severity="error" sx={{mt: 1}}>{t("account_usage_tier_payment_overdue")}</Alert> <Alert severity="error" sx={{mt: 1}}>{t("account_usage_tier_payment_overdue")}</Alert>
} }
{account.billing?.cancel_at > 0 && {account.billing?.cancel_at > 0 &&
@ -318,7 +324,7 @@ const Stats = () => {
{t("account_usage_title")} {t("account_usage_title")}
</Typography> </Typography>
<PrefGroup> <PrefGroup>
{account.role !== "admin" && {account.role === Role.USER &&
<Pref title={t("account_usage_reservations_title")}> <Pref title={t("account_usage_reservations_title")}>
{account.limits.reservations > 0 && {account.limits.reservations > 0 &&
<> <>
@ -326,7 +332,7 @@ const Stats = () => {
<Typography variant="body2" <Typography variant="body2"
sx={{float: "left"}}>{account.stats.reservations}</Typography> sx={{float: "left"}}>{account.stats.reservations}</Typography>
<Typography variant="body2" <Typography variant="body2"
sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")}</Typography> sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")}</Typography>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
@ -347,11 +353,11 @@ const Stats = () => {
}> }>
<div> <div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography> <Typography variant="body2" sx={{float: "left"}}>{account.stats.messages}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography> <Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}</Typography>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.role === "user" ? normalize(account.stats.messages, account.limits.messages) : 100} value={account.role === Role.USER ? normalize(account.stats.messages, account.limits.messages) : 100}
/> />
</Pref> </Pref>
<Pref title={ <Pref title={
@ -362,11 +368,11 @@ const Stats = () => {
}> }>
<div> <div>
<Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography> <Typography variant="body2" sx={{float: "left"}}>{account.stats.emails}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography> <Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}</Typography>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.role === "user" ? normalize(account.stats.emails, account.limits.emails) : 100} value={account.role === Role.USER ? normalize(account.stats.emails, account.limits.emails) : 100}
/> />
</Pref> </Pref>
<Pref <Pref
@ -382,15 +388,15 @@ const Stats = () => {
> >
<div> <div>
<Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography> <Typography variant="body2" sx={{float: "left"}}>{formatBytes(account.stats.attachment_total_size)}</Typography>
<Typography variant="body2" sx={{float: "right"}}>{account.role === "user" ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography> <Typography variant="body2" sx={{float: "right"}}>{account.role === Role.USER ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}</Typography>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={account.role === "user" ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100} value={account.role === Role.USER ? normalize(account.stats.attachment_total_size, account.limits.attachment_total_size) : 100}
/> />
</Pref> </Pref>
</PrefGroup> </PrefGroup>
{account.role === "user" && account.limits.basis === "ip" && {account.role === Role.USER && account.limits.basis === LimitBasis.IP &&
<Typography variant="body1"> <Typography variant="body1">
{t("account_usage_basis_ip_description")} {t("account_usage_basis_ip_description")}
</Typography> </Typography>

View file

@ -28,7 +28,7 @@ import config from "../app/config";
import ArticleIcon from '@mui/icons-material/Article'; import ArticleIcon from '@mui/icons-material/Article';
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import accountApi from "../app/AccountApi"; import accountApi, {Permission, Role} from "../app/AccountApi";
import CelebrationIcon from '@mui/icons-material/Celebration'; import CelebrationIcon from '@mui/icons-material/Celebration';
import UpgradeDialog from "./UpgradeDialog"; import UpgradeDialog from "./UpgradeDialog";
import {AccountContext} from "./App"; import {AccountContext} from "./App";
@ -104,7 +104,7 @@ const NavList = (props) => {
navigate(routes.account); navigate(routes.account);
}; };
const isAdmin = account?.role === "admin"; const isAdmin = account?.role === Role.ADMIN;
const isPaid = account?.billing?.subscription; const isPaid = account?.billing?.subscription;
const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid; const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;
const showSubscriptionsList = props.subscriptions?.length > 0; const showSubscriptionsList = props.subscriptions?.length > 0;
@ -264,16 +264,16 @@ const SubscriptionItem = (props) => {
<ListItemText primary={displayName} primaryTypographyProps={{ style: { overflow: "hidden", textOverflow: "ellipsis" } }}/> <ListItemText primary={displayName} primaryTypographyProps={{ style: { overflow: "hidden", textOverflow: "ellipsis" } }}/>
{subscription.reservation?.everyone && {subscription.reservation?.everyone &&
<ListItemIcon edge="end" sx={{ minWidth: "26px" }}> <ListItemIcon edge="end" sx={{ minWidth: "26px" }}>
{subscription.reservation?.everyone === "read-write" && {subscription.reservation?.everyone === Permission.READ_WRITE &&
<Tooltip title={t("prefs_reservations_table_everyone_read_write")}><PermissionReadWrite size="small"/></Tooltip> <Tooltip title={t("prefs_reservations_table_everyone_read_write")}><PermissionReadWrite size="small"/></Tooltip>
} }
{subscription.reservation?.everyone === "read-only" && {subscription.reservation?.everyone === Permission.READ_ONLY &&
<Tooltip title={t("prefs_reservations_table_everyone_read_only")}><PermissionRead size="small"/></Tooltip> <Tooltip title={t("prefs_reservations_table_everyone_read_only")}><PermissionRead size="small"/></Tooltip>
} }
{subscription.reservation?.everyone === "write-only" && {subscription.reservation?.everyone === Permission.WRITE_ONLY &&
<Tooltip title={t("prefs_reservations_table_everyone_write_only")}><PermissionWrite size="small"/></Tooltip> <Tooltip title={t("prefs_reservations_table_everyone_write_only")}><PermissionWrite size="small"/></Tooltip>
} }
{subscription.reservation?.everyone === "deny-all" && {subscription.reservation?.everyone === Permission.DENY_ALL &&
<Tooltip title={t("prefs_reservations_table_everyone_deny_all")}><PermissionDenyAll size="small"/></Tooltip> <Tooltip title={t("prefs_reservations_table_everyone_deny_all")}><PermissionDenyAll size="small"/></Tooltip>
} }
</ListItemIcon> </ListItemIcon>

View file

@ -39,7 +39,7 @@ import {playSound, shuffle, sounds, validTopic, validUrl} from "../app/utils";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi, {UnauthorizedError} from "../app/AccountApi"; import accountApi, {Permission, Role, UnauthorizedError} from "../app/AccountApi";
import {Pref, PrefGroup} from "./Pref"; import {Pref, PrefGroup} from "./Pref";
import LockIcon from "@mui/icons-material/Lock"; import LockIcon from "@mui/icons-material/Lock";
import {Info, Public, PublicOff} from "@mui/icons-material"; import {Info, Public, PublicOff} from "@mui/icons-material";
@ -485,11 +485,11 @@ const Reservations = () => {
const [dialogKey, setDialogKey] = useState(0); const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
if (!config.enable_reservations || !session.exists() || !account || account.role === "admin") { if (!config.enable_reservations || !session.exists() || !account || account.role === Role.ADMIN) {
return <></>; return <></>;
} }
const reservations = account.reservations || []; const reservations = account.reservations || [];
const limitReached = account.role === "user" && account.stats.reservations_remaining === 0; const limitReached = account.role === Role.USER && account.stats.reservations_remaining === 0;
const handleAddClick = () => { const handleAddClick = () => {
setDialogKey(prev => prev+1); setDialogKey(prev => prev+1);
@ -602,25 +602,25 @@ const ReservationsTable = (props) => {
{reservation.topic} {reservation.topic}
</TableCell> </TableCell>
<TableCell aria-label={t("prefs_reservations_table_access_header")}> <TableCell aria-label={t("prefs_reservations_table_access_header")}>
{reservation.everyone === "read-write" && {reservation.everyone === Permission.READ_WRITE &&
<> <>
<Public fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/> <Public fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
{t("prefs_reservations_table_everyone_read_write")} {t("prefs_reservations_table_everyone_read_write")}
</> </>
} }
{reservation.everyone === "read-only" && {reservation.everyone === Permission.READ_ONLY &&
<> <>
<PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/> <PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
{t("prefs_reservations_table_everyone_read_only")} {t("prefs_reservations_table_everyone_read_only")}
</> </>
} }
{reservation.everyone === "write-only" && {reservation.everyone === Permission.WRITE_ONLY &&
<> <>
<PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/> <PublicOff fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
{t("prefs_reservations_table_everyone_write_only")} {t("prefs_reservations_table_everyone_write_only")}
</> </>
} }
{reservation.everyone === "deny-all" && {reservation.everyone === Permission.DENY_ALL &&
<> <>
<LockIcon fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/> <LockIcon fontSize="small" sx={{color: "grey", verticalAlign: "bottom", mr: 0.5}}/>
{t("prefs_reservations_table_everyone_deny_all")} {t("prefs_reservations_table_everyone_deny_all")}

View file

@ -5,6 +5,7 @@ import MenuItem from "@mui/material/MenuItem";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText"; import ListItemText from "@mui/material/ListItemText";
import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons";
import {Permission} from "../app/AccountApi";
const ReserveTopicSelect = (props) => { const ReserveTopicSelect = (props) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -24,19 +25,19 @@ const ReserveTopicSelect = (props) => {
} }
}} }}
> >
<MenuItem value="deny-all"> <MenuItem value={Permission.DENY_ALL}>
<ListItemIcon><PermissionDenyAll/></ListItemIcon> <ListItemIcon><PermissionDenyAll/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/> <ListItemText primary={t("prefs_reservations_table_everyone_deny_all")}/>
</MenuItem> </MenuItem>
<MenuItem value="read-only"> <MenuItem value={Permission.READ_ONLY}>
<ListItemIcon><PermissionRead/></ListItemIcon> <ListItemIcon><PermissionRead/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/> <ListItemText primary={t("prefs_reservations_table_everyone_read_only")}/>
</MenuItem> </MenuItem>
<MenuItem value="write-only"> <MenuItem value={Permission.WRITE_ONLY}>
<ListItemIcon><PermissionWrite/></ListItemIcon> <ListItemIcon><PermissionWrite/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/> <ListItemText primary={t("prefs_reservations_table_everyone_write_only")}/>
</MenuItem> </MenuItem>
<MenuItem value="read-write"> <MenuItem value={Permission.READ_WRITE}>
<ListItemIcon><PermissionReadWrite/></ListItemIcon> <ListItemIcon><PermissionReadWrite/></ListItemIcon>
<ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/> <ListItemText primary={t("prefs_reservations_table_everyone_read_write")}/>
</MenuItem> </MenuItem>

View file

@ -17,7 +17,7 @@ import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi"; import accountApi, {Role, TopicReservedError, UnauthorizedError} from "../app/AccountApi";
import ReserveTopicSelect from "./ReserveTopicSelect"; import ReserveTopicSelect from "./ReserveTopicSelect";
import {AccountContext} from "./App"; import {AccountContext} from "./App";
@ -87,7 +87,7 @@ const SubscribePage = (props) => {
const existingBaseUrls = Array const existingBaseUrls = Array
.from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)])) .from(new Set([publicBaseUrl, ...props.subscriptions.map(s => s.baseUrl)]))
.filter(s => s !== config.base_url); .filter(s => s !== config.base_url);
const reserveTopicEnabled = session.exists() && account?.role === "user" && (account?.stats.reservations_remaining || 0) > 0; const reserveTopicEnabled = session.exists() && account?.role === Role.USER && (account?.stats.reservations_remaining || 0) > 0;
const handleSubscribe = async () => { const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined const user = await userManager.get(baseUrl); // May be undefined

View file

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import {useContext, useEffect, useState} from 'react';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
@ -6,16 +7,14 @@ import {Alert, CardActionArea, CardContent, ListItem, useMediaQuery} from "@mui/
import theme from "./theme"; import theme from "./theme";
import DialogFooter from "./DialogFooter"; import DialogFooter from "./DialogFooter";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import accountApi, {TopicReservedError, UnauthorizedError} from "../app/AccountApi"; import accountApi, {UnauthorizedError} from "../app/AccountApi";
import session from "../app/Session"; import session from "../app/Session";
import routes from "./routes"; import routes from "./routes";
import {useContext, useEffect, useState} from "react";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {AccountContext} from "./App"; import {AccountContext} from "./App";
import {formatBytes, formatNumber, formatShortDate} from "../app/utils"; import {formatBytes, formatNumber, formatShortDate} from "../app/utils";
import {Trans, useTranslation} from "react-i18next"; import {Trans, useTranslation} from "react-i18next";
import subscriptionManager from "../app/SubscriptionManager";
import List from "@mui/material/List"; import List from "@mui/material/List";
import {Check} from "@mui/icons-material"; import {Check} from "@mui/icons-material";
import ListItemIcon from "@mui/material/ListItemIcon"; import ListItemIcon from "@mui/material/ListItemIcon";