feat: optimize profile page

This commit is contained in:
GyDi 2022-08-14 23:10:19 +08:00
parent f1a68ece01
commit 33ce235713
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
12 changed files with 291 additions and 211 deletions

View File

@ -21,12 +21,16 @@ const EnhancedMode = (props: Props) => {
const { items, chain } = props;
const { mutate: mutateProfiles } = useSWR("getProfiles", getProfiles);
const { data: chainLogs = {} } = useSWR("getRuntimeLogs", getRuntimeLogs);
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
"getRuntimeLogs",
getRuntimeLogs
);
// handler
const onEnhance = useLockFn(async () => {
try {
await enhanceProfiles();
mutateLogs();
Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err.message || err.toString());
@ -39,6 +43,7 @@ const EnhancedMode = (props: Props) => {
const newChain = [...chain, uid];
await changeProfileChain(newChain);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateLogs();
});
const onEnhanceDisable = useLockFn(async (uid: string) => {
@ -47,6 +52,7 @@ const EnhancedMode = (props: Props) => {
const newChain = chain.filter((i) => i !== uid);
await changeProfileChain(newChain);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateLogs();
});
const onEnhanceDelete = useLockFn(async (uid: string) => {
@ -54,6 +60,7 @@ const EnhancedMode = (props: Props) => {
await onEnhanceDisable(uid);
await deleteProfile(uid);
mutateProfiles();
mutateLogs();
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
@ -65,6 +72,7 @@ const EnhancedMode = (props: Props) => {
const newChain = [uid].concat(chain.filter((i) => i !== uid));
await changeProfileChain(newChain);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateLogs();
});
const onMoveEnd = useLockFn(async (uid: string) => {
@ -73,10 +81,11 @@ const EnhancedMode = (props: Props) => {
const newChain = chain.filter((i) => i !== uid).concat([uid]);
await changeProfileChain(newChain);
mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true);
mutateLogs();
});
return (
<Box sx={{ mt: 4 }}>
<Box sx={{ mt: 2 }}>
<Stack
spacing={1}
direction="row"

View File

@ -82,7 +82,9 @@ const FileEditor = (props: Props) => {
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t("Cancel")}</Button>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={onSave} variant="contained">
{t("Save")}
</Button>

View File

@ -1,5 +1,6 @@
import { useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Box, Button, Typography } from "@mui/material";
interface Props {
@ -9,6 +10,7 @@ interface Props {
const FileInput = (props: Props) => {
const { onChange } = props;
const { t } = useTranslation();
// file input
const inputRef = useRef<any>();
const [loading, setLoading] = useState(false);
@ -40,7 +42,7 @@ const FileInput = (props: Props) => {
sx={{ flex: "none" }}
onClick={() => inputRef.current?.click()}
>
Choose File
{t("Choose File")}
</Button>
<input

View File

@ -23,7 +23,7 @@ interface Props {
// edit the profile item
// remote / local file / merge / script
const ProfileEdit = (props: Props) => {
const InfoEditor = (props: Props) => {
const { open, itemData, onClose } = props;
const { t } = useTranslation();
@ -56,7 +56,6 @@ const ProfileEdit = (props: Props) => {
}
await patchProfile(uid, { uid, name, desc, url, option: option_ });
setShowOpt(false);
mutate("getProfiles");
onClose();
} catch (err: any) {
@ -133,7 +132,7 @@ const ProfileEdit = (props: Props) => {
value={option.update_interval}
onChange={(e) => {
const str = e.target.value?.replace(/\D/, "");
setOption({ update_interval: str != null ? +str : str });
setOption({ update_interval: !!str ? +str : undefined });
}}
onKeyDown={(e) => e.key === "Enter" && onUpdate()}
/>
@ -144,6 +143,7 @@ const ProfileEdit = (props: Props) => {
{form.type === "remote" && (
<IconButton
size="small"
color="inherit"
sx={{ position: "absolute", left: 18 }}
onClick={() => setShowOpt((o) => !o)}
>
@ -151,13 +151,15 @@ const ProfileEdit = (props: Props) => {
</IconButton>
)}
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={onUpdate} variant="contained">
Update
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
export default ProfileEdit;
export default InfoEditor;

View File

@ -0,0 +1,71 @@
import { useTranslation } from "react-i18next";
import {
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Divider,
Typography,
} from "@mui/material";
import BaseEmpty from "../base/base-empty";
import { Fragment } from "react";
interface Props {
open: boolean;
logInfo: [string, string][];
onClose: () => void;
}
const LogViewer = (props: Props) => {
const { open, logInfo, onClose } = props;
const { t } = useTranslation();
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{t("Script Console")}</DialogTitle>
<DialogContent
sx={{
width: 400,
height: 300,
overflowX: "hidden",
userSelect: "text",
pb: 1,
}}
>
{logInfo.map(([level, log], index) => (
<Fragment key={index.toString()}>
<Typography color="text.secondary" component="div">
<Chip
label={level}
size="small"
variant="outlined"
color={
level === "error" || level === "exception"
? "error"
: "default"
}
sx={{ mr: 1 }}
/>
{log}
</Typography>
<Divider sx={{ my: 0.5 }} />
</Fragment>
))}
{logInfo.length === 0 && <BaseEmpty />}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Back")}
</Button>
</DialogActions>
</Dialog>
);
};
export default LogViewer;

View File

@ -0,0 +1,43 @@
import { alpha, Box, styled } from "@mui/material";
const ProfileBox = styled(Box)(({ theme, "aria-selected": selected }) => {
const { mode, primary, text, grey, background } = theme.palette;
const key = `${mode}-${!!selected}`;
const backgroundColor = {
"light-true": alpha(primary.main, 0.2),
"light-false": alpha(background.paper, 0.75),
"dark-true": alpha(primary.main, 0.45),
"dark-false": alpha(grey[700], 0.45),
}[key]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.85),
"dark-false": alpha(text.secondary, 0.65),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.light,
"dark-false": text.primary,
}[key]!;
return {
width: "100%",
display: "block",
cursor: "pointer",
textAlign: "left",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
padding: "8px 16px",
boxSizing: "border-box",
backgroundColor,
color,
"& h2": { color: h2color },
};
});
export default ProfileBox;

View File

@ -1,13 +1,11 @@
import dayjs from "dayjs";
import { mutate } from "swr";
import { useEffect, useState } from "react";
import { useLockFn } from "ahooks";
import { useSWRConfig } from "swr";
import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next";
import {
alpha,
Box,
styled,
Typography,
LinearProgress,
IconButton,
@ -19,21 +17,11 @@ import { RefreshRounded } from "@mui/icons-material";
import { atomLoadingCache } from "@/services/states";
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
import parseTraffic from "@/utils/parse-traffic";
import ProfileEdit from "./profile-edit";
import ProfileBox from "./profile-box";
import InfoEditor from "./info-editor";
import FileEditor from "./file-editor";
import Notice from "../base/base-notice";
const Wrapper = styled(Box)(({ theme }) => ({
width: "100%",
display: "block",
cursor: "pointer",
textAlign: "left",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
padding: "8px 16px",
boxSizing: "border-box",
}));
const round = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
@ -49,7 +37,6 @@ const ProfileItem = (props: Props) => {
const { selected, itemData, onSelect } = props;
const { t } = useTranslation();
const { mutate } = useSWRConfig();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache);
@ -58,7 +45,6 @@ const ProfileItem = (props: Props) => {
// local file mode
// remote file mode
// subscription url mode
const hasUrl = !!itemData.url;
const hasExtra = !!extra; // only subscription url has extra info
@ -79,7 +65,6 @@ const ProfileItem = (props: Props) => {
const handler = () => {
const now = Date.now();
const lastUpdate = updated * 1000;
// 大于一天的不管
if (now - lastUpdate >= 24 * 36e5) return;
@ -152,13 +137,6 @@ const ProfileItem = (props: Props) => {
}
});
const boxStyle = {
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
};
const urlModeMenu = [
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
@ -176,36 +154,17 @@ const ProfileItem = (props: Props) => {
{ label: "Delete", handler: onDelete },
];
const boxStyle = {
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
};
return (
<>
<Wrapper
sx={({ palette }) => {
const { mode, primary, text, grey } = palette;
const key = `${mode}-${selected}`;
const bgcolor = {
"light-true": alpha(primary.main, 0.15),
"light-false": palette.background.paper,
"dark-true": alpha(primary.main, 0.35),
"dark-false": alpha(grey[700], 0.35),
}[key]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.75),
"dark-false": alpha(text.secondary, 0.75),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.light,
"dark-false": text.primary,
}[key]!;
return { bgcolor, color, "& h2": { color: h2color } };
}}
<ProfileBox
aria-selected={selected}
onClick={() => onSelect(false)}
onContextMenu={(event) => {
const { clientX, clientY } = event;
@ -214,9 +173,9 @@ const ProfileItem = (props: Props) => {
event.preventDefault();
}}
>
<Box display="flex" justifyContent="space-between">
<Box position="relative">
<Typography
width="calc(100% - 40px)"
width="calc(100% - 36px)"
variant="h6"
component="h2"
noWrap
@ -229,10 +188,13 @@ const ProfileItem = (props: Props) => {
{hasUrl && (
<IconButton
sx={{
width: 26,
height: 26,
position: "absolute",
p: "3px",
top: -1,
right: -5,
animation: loading ? `1s linear infinite ${round}` : "none",
}}
size="small"
color="inherit"
disabled={loading}
onClick={(e) => {
@ -240,47 +202,47 @@ const ProfileItem = (props: Props) => {
onUpdate(false);
}}
>
<RefreshRounded />
<RefreshRounded color="inherit" />
</IconButton>
)}
</Box>
{/* the second line show url's info or description */}
{hasUrl ? (
<Box sx={boxStyle}>
<Typography noWrap title={`From: ${from}`}>
{from}
</Typography>
<Box sx={boxStyle}>
{hasUrl ? (
<>
<Typography noWrap title={`From: ${from}`}>
{from}
</Typography>
<Typography
noWrap
flex="1 0 auto"
fontSize={14}
textAlign="right"
title="updated time"
>
{updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
</Typography>
</Box>
) : (
<Box sx={boxStyle}>
<Typography
noWrap
flex="1 0 auto"
fontSize={14}
textAlign="right"
title={`Updated Time: ${parseExpire(updated)}`}
>
{updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
</Typography>
</>
) : (
<Typography noWrap title={itemData.desc}>
{itemData.desc}
</Typography>
</Box>
)}
)}
</Box>
{/* the third line show extra info or last updated time */}
{hasExtra ? (
<Box sx={{ ...boxStyle, fontSize: 14 }}>
<span title="used / total">
<span title="Used / Total">
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
<span title="expire time">{expire}</span>
<span title="Expire Time">{expire}</span>
</Box>
) : (
<Box sx={{ ...boxStyle, fontSize: 14, justifyContent: "flex-end" }}>
<span title="updated time">{parseExpire(updated)}</span>
<span title="Updated Time">{parseExpire(updated)}</span>
</Box>
)}
@ -289,7 +251,7 @@ const ProfileItem = (props: Props) => {
value={progress}
color="inherit"
/>
</Wrapper>
</ProfileBox>
<Menu
open={!!anchorEl}
@ -314,22 +276,18 @@ const ProfileItem = (props: Props) => {
))}
</Menu>
{editOpen && (
<ProfileEdit
open={editOpen}
itemData={itemData}
onClose={() => setEditOpen(false)}
/>
)}
<InfoEditor
open={editOpen}
itemData={itemData}
onClose={() => setEditOpen(false)}
/>
{fileOpen && (
<FileEditor
uid={uid}
open={fileOpen}
mode="yaml"
onClose={() => setFileOpen(false)}
/>
)}
<FileEditor
uid={uid}
open={fileOpen}
mode="yaml"
onClose={() => setFileOpen(false)}
/>
</>
);
};

View File

@ -1,32 +1,24 @@
import dayjs from "dayjs";
import { useEffect, useState } from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import {
alpha,
Box,
Badge,
Chip,
styled,
Typography,
MenuItem,
Menu,
IconButton,
} from "@mui/material";
import { FeaturedPlayListRounded } from "@mui/icons-material";
import { viewProfile } from "@/services/cmds";
import ProfileEdit from "./profile-edit";
import InfoEditor from "./info-editor";
import FileEditor from "./file-editor";
import ProfileBox from "./profile-box";
import LogViewer from "./log-viewer";
import Notice from "../base/base-notice";
const Wrapper = styled(Box)(({ theme }) => ({
width: "100%",
display: "block",
cursor: "pointer",
textAlign: "left",
borderRadius: theme.shape.borderRadius,
boxShadow: theme.shadows[2],
padding: "8px 16px",
boxSizing: "border-box",
}));
interface Props {
selected: boolean;
itemData: CmdType.ProfileItem;
@ -55,18 +47,11 @@ const ProfileMore = (props: Props) => {
const { uid, type } = itemData;
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [editOpen, setEditOpen] = useState(false);
const [fileOpen, setFileOpen] = useState(false);
// const [status, setStatus] = useState(enhance.status(uid));
// unlisten when unmount
// useEffect(() => enhance.listen(uid, setStatus), [uid]);
// error during enhanced mode
const hasError = !!logInfo.find((e) => e[0] === "exception"); // selected && status?.status === "error";
const [logOpen, setLogOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null);
@ -92,6 +77,7 @@ const ProfileMore = (props: Props) => {
return fn();
};
const hasError = !!logInfo.find((e) => e[0] === "exception");
const showMove = enableNum > 1 && !hasError;
const enableMenu = [
@ -122,39 +108,8 @@ const ProfileMore = (props: Props) => {
return (
<>
<Wrapper
sx={({ palette }) => {
// todo
// 区分 selected 和 error 和 mode 下各种颜色的排列组合
const { mode, primary, text, grey, error } = palette;
const key = `${mode}-${selected}`;
const bgkey = hasError ? `${mode}-err` : key;
const bgcolor = {
"light-true": alpha(primary.main, 0.15),
"light-false": palette.background.paper,
"dark-true": alpha(primary.main, 0.35),
"dark-false": alpha(grey[700], 0.35),
"light-err": alpha(error.main, 0.12),
"dark-err": alpha(error.main, 0.3),
}[bgkey]!;
const color = {
"light-true": text.secondary,
"light-false": text.secondary,
"dark-true": alpha(text.secondary, 0.6),
"dark-false": alpha(text.secondary, 0.6),
}[key]!;
const h2color = {
"light-true": primary.main,
"light-false": text.primary,
"dark-true": primary.light,
"dark-false": text.primary,
}[key]!;
return { bgcolor, color, "& h2": { color: h2color } };
}}
<ProfileBox
aria-selected={selected}
// onClick={() => onSelect(false)}
onContextMenu={(event) => {
const { clientX, clientY } = event;
@ -163,7 +118,12 @@ const ProfileMore = (props: Props) => {
event.preventDefault();
}}
>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Box
display="flex"
justifyContent="space-between"
alignItems="center"
mb={0.5}
>
<Typography
width="calc(100% - 52px)"
variant="h6"
@ -179,22 +139,33 @@ const ProfileMore = (props: Props) => {
color="primary"
size="small"
variant="outlined"
sx={{ textTransform: "capitalize" }}
sx={{ height: 20, textTransform: "capitalize" }}
/>
</Box>
<Box sx={boxStyle}>
{hasError ? (
<Typography
noWrap
color="error"
sx={{ width: "calc(100% - 75px)" }}
// title={status.message}
title="error"
>
{/* {status.message} */}
error
</Typography>
{selected ? (
hasError ? (
<Badge color="primary" variant="dot" overlap="circular">
<IconButton
size="small"
edge="start"
color="error"
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
</IconButton>
</Badge>
) : (
<IconButton
size="small"
edge="start"
color="inherit"
onClick={() => setLogOpen(true)}
>
<FeaturedPlayListRounded fontSize="inherit" />
</IconButton>
)
) : (
<Typography
noWrap
@ -207,13 +178,15 @@ const ProfileMore = (props: Props) => {
<Typography
component="span"
title="updated time"
title={`Updated Time: ${parseExpire(itemData.updated)}`}
style={{ fontSize: 14 }}
>
{parseExpire(itemData.updated)}
{!!itemData.updated
? dayjs(itemData.updated! * 1000).fromNow()
: ""}
</Typography>
</Box>
</Wrapper>
</ProfileBox>
<Menu
open={!!anchorEl}
@ -240,20 +213,24 @@ const ProfileMore = (props: Props) => {
))}
</Menu>
{editOpen && (
<ProfileEdit
open={editOpen}
itemData={itemData}
onClose={() => setEditOpen(false)}
/>
)}
<InfoEditor
open={editOpen}
itemData={itemData}
onClose={() => setEditOpen(false)}
/>
{fileOpen && (
<FileEditor
uid={uid}
open={fileOpen}
mode={type === "merge" ? "yaml" : "javascript"}
onClose={() => setFileOpen(false)}
<FileEditor
uid={uid}
open={fileOpen}
mode={type === "merge" ? "yaml" : "javascript"}
onClose={() => setFileOpen(false)}
/>
{selected && (
<LogViewer
open={logOpen}
logInfo={logInfo}
onClose={() => setLogOpen(false)}
/>
)}
</>

View File

@ -1,5 +1,6 @@
import { useRef, useState } from "react";
import { useSWRConfig } from "swr";
import { mutate } from "swr";
import { useTranslation } from "react-i18next";
import { useLockFn, useSetState } from "ahooks";
import {
Button,
@ -29,7 +30,7 @@ interface Props {
const ProfileNew = (props: Props) => {
const { open, onClose } = props;
const { mutate } = useSWRConfig();
const { t } = useTranslation();
const [form, setForm] = useSetState({
type: "remote",
name: "",
@ -83,7 +84,7 @@ const ProfileNew = (props: Props) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle sx={{ pb: 0.5 }}>Create Profile</DialogTitle>
<DialogTitle sx={{ pb: 0.5 }}>{t("Create Profile")}</DialogTitle>
<DialogContent sx={{ width: 336, pb: 1 }}>
<FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
@ -120,7 +121,7 @@ const ProfileNew = (props: Props) => {
{form.type === "remote" && (
<TextField
{...textFieldProps}
label="Subscription Url"
label="Subscription URL"
autoComplete="off"
value={form.url}
onChange={(e) => setForm({ url: e.target.value })}
@ -146,6 +147,7 @@ const ProfileNew = (props: Props) => {
{form.type === "remote" && (
<IconButton
size="small"
color="inherit"
sx={{ position: "absolute", left: 18 }}
onClick={() => setShowOpt((o) => !o)}
>
@ -153,9 +155,11 @@ const ProfileNew = (props: Props) => {
</IconButton>
)}
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={onCreate} variant="contained">
Create
{t("Save")}
</Button>
</DialogActions>
</Dialog>

View File

@ -18,6 +18,8 @@
"Profile URL": "Profile URL",
"Import": "Import",
"New": "New",
"Create Profile": "Create Profile",
"Choose File": "Choose File",
"Close All": "Close All",
"Select": "Select",
"Edit Info": "Edit Info",

View File

@ -18,6 +18,8 @@
"Profile URL": "配置文件链接",
"Import": "导入",
"New": "新建",
"Create Profile": "新建配置",
"Choose File": "选择文件",
"Close All": "关闭全部",
"Select": "使用",
"Edit Info": "编辑信息",

View File

@ -1,8 +1,8 @@
import useSWR, { useSWRConfig } from "swr";
import useSWR, { mutate } from "swr";
import { useLockFn } from "ahooks";
import { useEffect, useMemo, useState } from "react";
import { useSetRecoilState } from "recoil";
import { Box, Button, Grid, TextField } from "@mui/material";
import { Button, Grid, Stack, TextField } from "@mui/material";
import { useTranslation } from "react-i18next";
import {
getProfiles,
@ -20,7 +20,6 @@ import EnhancedMode from "@/components/profile/enhanced";
const ProfilePage = () => {
const { t } = useTranslation();
const { mutate } = useSWRConfig();
const [url, setUrl] = useState("");
const [disabled, setDisabled] = useState(false);
@ -110,10 +109,13 @@ const ProfilePage = () => {
getProfiles().then((newProfiles) => {
mutate("getProfiles", newProfiles);
if (!newProfiles.current && newProfiles.items?.length) {
const current = newProfiles.items[0].uid;
const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
if (!newProfiles.current && remoteItem) {
const current = remoteItem.uid;
selectProfile(current);
mutate("getProfiles", { ...newProfiles, current }, true);
mutate("getRuntimeLogs");
}
});
} catch {
@ -130,6 +132,7 @@ const ProfilePage = () => {
await selectProfile(uid);
setCurrentProfile(uid);
mutate("getProfiles", { ...profiles, current: uid }, true);
mutate("getRuntimeLogs");
if (force) Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
@ -138,29 +141,34 @@ const ProfilePage = () => {
return (
<BasePage title={t("Profiles")}>
<Box sx={{ display: "flex", mb: 2.5 }}>
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
<TextField
id="clas_verge_profile_url"
name="profile_url"
label={t("Profile URL")}
size="small"
hiddenLabel
fullWidth
size="small"
value={url}
variant="outlined"
autoComplete="off"
onChange={(e) => setUrl(e.target.value)}
sx={{ mr: 1 }}
sx={{ input: { py: 0.65, px: 1.25 } }}
placeholder={t("Profile URL")}
/>
<Button
disabled={!url || disabled}
variant="contained"
size="small"
onClick={onImport}
sx={{ mr: 1 }}
>
{t("Import")}
</Button>
<Button variant="contained" onClick={() => setDialogOpen(true)}>
<Button
variant="contained"
size="small"
onClick={() => setDialogOpen(true)}
>
{t("New")}
</Button>
</Box>
</Stack>
<Grid container spacing={2}>
{regularItems.map((item) => (