mirror of
https://github.com/nix-community/noogle.git
synced 2024-11-27 05:55:03 +03:00
Pkgs search: add more metadata
This commit is contained in:
parent
75ebb3c276
commit
9299c33700
@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { FilterProvider } from "@/components/layout/filterContext";
|
||||
import { Header } from "@/components/layout/header";
|
||||
import { PkgsSearch } from "@/components/searchInput/pkgsSearch";
|
||||
@ -16,6 +17,7 @@ export default function SearchLayout({
|
||||
<Suspense fallback="query">
|
||||
<FilterProvider queryUrl="pkgs">
|
||||
<Header search={<PkgsSearch placeholder="Search packages" />} />
|
||||
|
||||
<Container
|
||||
disableGutters
|
||||
maxWidth="lg"
|
||||
|
@ -1,5 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import { EmptyRecordsPlaceholder } from "@/components/emptyRecordsPlaceholder";
|
||||
import { useFilter } from "@/components/layout/filterContext";
|
||||
import Clear from "@mui/icons-material/Clear";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
Collapse,
|
||||
Container,
|
||||
Divider,
|
||||
Icon,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
import CodeIcon from "@mui/icons-material/Code";
|
||||
|
||||
import { ExpandLess, ExpandMore, GitHub, Mail } from "@mui/icons-material";
|
||||
|
||||
const getPayload = (term: string) => {
|
||||
const extraWildcards = term.split(" ").map((t) => ({
|
||||
wildcard: {
|
||||
@ -189,18 +215,29 @@ type Hit = {
|
||||
|
||||
type PackageItemProps = {
|
||||
item: Hit;
|
||||
selected: boolean;
|
||||
setSelected: (id: string) => void;
|
||||
};
|
||||
const PackageItem = (props: PackageItemProps) => {
|
||||
const { item } = props;
|
||||
const { item, selected, setSelected } = props;
|
||||
|
||||
const { _source: meta } = item;
|
||||
|
||||
console.log({ meta, item });
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
sx={{ px: 1, py: 2, bgcolor: "background.paper", my: 1 }}
|
||||
aria-label={`item-${item._source.package_pname}`}
|
||||
>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
variant: "h5",
|
||||
@ -209,10 +246,42 @@ const PackageItem = (props: PackageItemProps) => {
|
||||
secondaryTypographyProps={{
|
||||
component: "div",
|
||||
variant: "body1",
|
||||
color: "text.primary",
|
||||
color: "text.secondary",
|
||||
}}
|
||||
primary={<Link href={"#"}>{meta.package_attr_name}</Link>}
|
||||
primary={
|
||||
<Link
|
||||
href={"#"}
|
||||
onClick={() => {
|
||||
setSelected(item._id);
|
||||
}}
|
||||
>
|
||||
{meta.package_attr_name}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
<Box
|
||||
sx={{ maxWidth: "sm" }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: meta.package_description,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
}}
|
||||
secondary={"name"}
|
||||
primary={meta.package_pname}
|
||||
/>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -221,8 +290,6 @@ const PackageItem = (props: PackageItemProps) => {
|
||||
secondary={"version"}
|
||||
primary={meta.package_pversion}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -243,35 +310,14 @@ const PackageItem = (props: PackageItemProps) => {
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
component: "div",
|
||||
variant: "body1",
|
||||
maxWidth: "40rem",
|
||||
}}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
secondary="description"
|
||||
primary={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: meta.package_description,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ display: "flex", gap: 2, mt: 1 }}>
|
||||
<Link href={meta.package_homepage?.[0] || "#"}>Homepage</Link>
|
||||
<Link
|
||||
href={`https://github.com/NixOS/nixpkgs/blob/nixos-24.05/${meta.package_position.replace(
|
||||
":",
|
||||
"#L"
|
||||
)}`}
|
||||
>
|
||||
Source
|
||||
</Link>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
@ -285,31 +331,177 @@ const PackageItem = (props: PackageItemProps) => {
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
></ListItemText>
|
||||
/>
|
||||
<Button
|
||||
sx={{
|
||||
ml: "auto",
|
||||
}}
|
||||
component={Link}
|
||||
href={meta.package_homepage?.[0] || "#"}
|
||||
startIcon={<LinkIcon fontSize="inherit" />}
|
||||
>
|
||||
Homepage
|
||||
</Button>
|
||||
<Button
|
||||
component={Link}
|
||||
href={`https://github.com/NixOS/nixpkgs/blob/nixos-unstable/${meta.package_position.replace(
|
||||
":",
|
||||
"#L"
|
||||
)}`}
|
||||
startIcon={<CodeIcon fontSize="inherit" />}
|
||||
>
|
||||
Source
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
mt: "-1rem",
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => setSelected(item._id)}>
|
||||
{selected ? <ExpandLess /> : <ExpandMore />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Collapse in={selected}>
|
||||
<Divider flexItem orientation="horizontal" />
|
||||
{/* <Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={() => setSelected(item._id)}>
|
||||
<ExpandLess />
|
||||
</IconButton>
|
||||
</Box> */}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primaryTypographyProps={{
|
||||
component: "div",
|
||||
variant: "body1",
|
||||
color: "text.primary",
|
||||
}}
|
||||
// secondaryTypographyProps={{
|
||||
// }}
|
||||
primary={
|
||||
<Box
|
||||
sx={{ width: "100%" }}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: meta.package_longDescription,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
// secondary={
|
||||
// ""
|
||||
// }
|
||||
/>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
}}
|
||||
secondary="programs"
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "0.5rem",
|
||||
color: "primary.main",
|
||||
}}
|
||||
>
|
||||
{meta.package_programs.map((output) => (
|
||||
<code key={output}>{output}</code>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
}}
|
||||
secondary="Platforms"
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
gap: "0.5rem",
|
||||
color: "primary.main",
|
||||
}}
|
||||
>
|
||||
{meta.package_platforms.map((output) => (
|
||||
<code key={output}>{output}</code>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListItemText
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
}}
|
||||
secondary="Maintainers"
|
||||
primary={
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "0.5rem",
|
||||
color: "primary.main",
|
||||
}}
|
||||
>
|
||||
{meta.package_maintainers.map((maintainer) => (
|
||||
<Box
|
||||
key={maintainer.github}
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "0.5rem",
|
||||
}}
|
||||
>
|
||||
<Link href={`https://github.com/${maintainer.github}`}>
|
||||
<Icon fontSize="inherit">
|
||||
<GitHub fontSize="inherit" />
|
||||
</Icon>
|
||||
{maintainer.name}
|
||||
</Link>
|
||||
<Box component={"span"} sx={{ color: "text.primary" }}>
|
||||
via
|
||||
</Box>
|
||||
<Link href={`mailto:${maintainer.email}`}>
|
||||
<Icon fontSize="inherit">
|
||||
<Mail fontSize="inherit" />
|
||||
</Icon>
|
||||
{maintainer.email}
|
||||
</Link>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
import { EmptyRecordsPlaceholder } from "@/components/emptyRecordsPlaceholder";
|
||||
import { useFilter } from "@/components/layout/filterContext";
|
||||
import Clear from "@mui/icons-material/Clear";
|
||||
import {
|
||||
Box,
|
||||
Chip,
|
||||
Container,
|
||||
IconButton,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import AppsIcon from "@mui/icons-material/Apps";
|
||||
|
||||
export default function Page() {
|
||||
const query = useSearchParams();
|
||||
@ -319,10 +511,14 @@ export default function Page() {
|
||||
const [items, setItems] = useState<Hit[]>();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const [selected, setSelected] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (term) {
|
||||
setLoading(true);
|
||||
fetch(`https://search.nixos.org/backend/latest-42-nixos-24.05/_search`, {
|
||||
fetch(
|
||||
`https://search.nixos.org/backend/latest-42-nixos-unstable/_search`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(getPayload(term)),
|
||||
headers: {
|
||||
@ -330,7 +526,8 @@ export default function Page() {
|
||||
Authorization:
|
||||
"Basic YVdWU0FMWHBadjpYOGdQSG56TDUyd0ZFZWt1eHNmUTljU2g=",
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setResults(data);
|
||||
@ -361,8 +558,38 @@ export default function Page() {
|
||||
<Container maxWidth="lg" sx={{ mt: 2 }}>
|
||||
{loading ? (
|
||||
<LinearProgress sx={{ my: 2 }} />
|
||||
) : !term ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
mt: "3.7rem",
|
||||
justifySelf: "center",
|
||||
width: "100%",
|
||||
px: 2,
|
||||
mx: 1,
|
||||
}}
|
||||
>
|
||||
<EmptyRecordsPlaceholder
|
||||
CardProps={{
|
||||
sx: { backgroundColor: "inherit" },
|
||||
}}
|
||||
title="Type something to search"
|
||||
icon={<AppsIcon fontSize="inherit" />}
|
||||
/>
|
||||
<Box sx={{ marginLeft: "auto" }}>
|
||||
<Link rel="canonical" href="/">
|
||||
Or search functions
|
||||
</Link>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle1"
|
||||
@ -377,6 +604,22 @@ export default function Page() {
|
||||
sx={{ color: "primary.contrastText" }}
|
||||
/>
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
order: 1,
|
||||
p: 1,
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`Channel: nixos-unstable`}
|
||||
color="primary"
|
||||
sx={{ color: "primary.contrastText" }}
|
||||
/>
|
||||
</Typography>
|
||||
{term && (
|
||||
<Typography
|
||||
component="span"
|
||||
@ -404,11 +647,22 @@ export default function Page() {
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<List aria-label="basic-list" sx={{ pt: 1, width: "100%" }}>
|
||||
{items?.length ? (
|
||||
items.map((item, idx) => (
|
||||
<React.Fragment key={`${idx}`}>
|
||||
<PackageItem item={item} />
|
||||
<PackageItem
|
||||
item={item}
|
||||
selected={item._id === selected}
|
||||
setSelected={(id) => {
|
||||
if (id === selected) {
|
||||
setSelected(null);
|
||||
} else {
|
||||
setSelected(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))
|
||||
) : (
|
||||
|
@ -1,73 +0,0 @@
|
||||
"use client";
|
||||
import { ChevronRight, ExpandMore } from "@mui/icons-material";
|
||||
import {
|
||||
Collapse,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { ReactNode, useState } from "react";
|
||||
|
||||
const borderStyle = {
|
||||
borderInlineStartStyle: "solid",
|
||||
borderInlineStartWidth: "1px",
|
||||
borderInlineStartColor: "#e4e4e7",
|
||||
ml: 2,
|
||||
pl: 2,
|
||||
};
|
||||
interface CollapseWrapperProps {
|
||||
children: ReactNode;
|
||||
name: string;
|
||||
position: number;
|
||||
}
|
||||
export const CollapseWrapper = (props: CollapseWrapperProps) => {
|
||||
const { children, name, position } = props;
|
||||
const [expandedKeys, setExpanded] = useState<string[]>([]);
|
||||
const isExpanded = expandedKeys.includes(name);
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={{
|
||||
...(position > 0 ? borderStyle : undefined),
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
disableGutters
|
||||
onClick={() => {
|
||||
setExpanded((s) =>
|
||||
isExpanded ? s.filter((n) => n !== name) : [...s, name]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
primaryTypographyProps={{
|
||||
sx: {
|
||||
fontWeight: 700,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ListItemIcon sx={{ minWidth: "40px" }}>
|
||||
{isExpanded ? <ExpandMore /> : <ChevronRight />}
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
<Collapse in={isExpanded}>
|
||||
<List
|
||||
dense
|
||||
disablePadding
|
||||
sx={{
|
||||
...(position > 0 ? borderStyle : undefined),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
"use client";
|
||||
import { ListItemButton } from "@mui/material";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
interface ListIndicatorProps {
|
||||
currentPath: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export const ListEntry = (props: ListIndicatorProps) => {
|
||||
const { currentPath, children } = props;
|
||||
const pathname = usePathname();
|
||||
const selected = pathname === currentPath;
|
||||
return (
|
||||
<ListItemButton disableGutters selected={selected}>
|
||||
{children}
|
||||
</ListItemButton>
|
||||
);
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
import { Group, getMdxMeta } from "@/utils";
|
||||
import { ListItem, ListItemText } from "@mui/material";
|
||||
import Link from "next/link";
|
||||
import { CollapseWrapper } from "./ListCollapse";
|
||||
import { ListEntry } from "./ListEntry";
|
||||
|
||||
interface ListGroupProps {
|
||||
grp: Group;
|
||||
position: number;
|
||||
}
|
||||
export const ListGroup = async (props: ListGroupProps) => {
|
||||
const { grp, position } = props;
|
||||
const sorted = Object.entries(grp).sort(([, v], [,]) =>
|
||||
Array.isArray(v) ? 1 : -1
|
||||
);
|
||||
|
||||
return sorted.map(async ([name, entry], idx) => {
|
||||
if (Array.isArray(entry)) {
|
||||
const matter = await getMdxMeta(entry);
|
||||
const { frontmatter } = matter.compiled;
|
||||
return (
|
||||
<Link rel="canonical" key={`${idx}`} href={`/ref/${entry.join("/")}`}>
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={{
|
||||
ml: 2,
|
||||
pl: 2,
|
||||
borderInlineStartStyle: "solid",
|
||||
borderInlineStartWidth: "1px",
|
||||
borderInlineStartColor: "#e4e4e7",
|
||||
}}
|
||||
>
|
||||
<ListEntry currentPath={`/ref/${entry.join("/")}`}>
|
||||
<ListItemText
|
||||
primary={
|
||||
frontmatter.path
|
||||
? frontmatter.path.join(".")
|
||||
: frontmatter.title
|
||||
}
|
||||
/>
|
||||
</ListEntry>
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<CollapseWrapper key={idx} name={name} position={position}>
|
||||
<ListGroup grp={entry} position={position + 1} />
|
||||
</CollapseWrapper>
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { Group, generateStaticSidebarEntries, set } from "@/utils";
|
||||
import { Box, List, Paper, Typography } from "@mui/material";
|
||||
import { ListGroup } from "./ListGroup";
|
||||
|
||||
export const NavSidebar = async () => {
|
||||
const list = await generateStaticSidebarEntries();
|
||||
const grp: Group = list.reduce((acc, item) => {
|
||||
set(acc, item.id.join("."), item.id);
|
||||
return acc;
|
||||
}, {});
|
||||
return (
|
||||
<Paper sx={{ height: "100%" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
py: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" component="h3">
|
||||
References
|
||||
</Typography>
|
||||
</Box>
|
||||
<List
|
||||
sx={{ width: "100%", bgcolor: "background.paper" }}
|
||||
component="nav"
|
||||
disablePadding
|
||||
>
|
||||
<ListGroup grp={grp} position={0} />
|
||||
</List>
|
||||
</Paper>
|
||||
);
|
||||
};
|
@ -1,10 +1,14 @@
|
||||
"use client";
|
||||
import { Box, LinearProgress, Link } from "@mui/material";
|
||||
import { Box, Button, LinearProgress, Link } from "@mui/material";
|
||||
import { Filter } from "../filter";
|
||||
import { ReactNode, Suspense } from "react";
|
||||
import { SocialIcons } from "./layout";
|
||||
|
||||
import localFont from "next/font/local";
|
||||
import { styled } from "@mui/material/styles";
|
||||
|
||||
const HeaderLink = styled(Link)(({ theme }) => ({
|
||||
color: theme.palette.primary.contrastText + "!important",
|
||||
}));
|
||||
|
||||
const fira = localFont({
|
||||
src: "../../fonts/FiraCode-VF.ttf",
|
||||
@ -45,9 +49,11 @@ export const Header = (props: HeaderProps) => {
|
||||
display: { xs: "none", md: "block" },
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
<Button
|
||||
// component={Link}
|
||||
rel="canonical"
|
||||
href="/"
|
||||
LinkComponent={HeaderLink}
|
||||
className={fira.className}
|
||||
sx={{
|
||||
color: "primary.contrastText",
|
||||
@ -56,7 +62,7 @@ export const Header = (props: HeaderProps) => {
|
||||
aria-label="Home"
|
||||
>
|
||||
{"Noogλe"}
|
||||
</Link>
|
||||
</Button>
|
||||
</Box>
|
||||
{/* <Box
|
||||
sx={{
|
||||
@ -69,7 +75,7 @@ export const Header = (props: HeaderProps) => {
|
||||
<Home />
|
||||
</IconButton>
|
||||
</Box> */}
|
||||
|
||||
{search && (
|
||||
<Box
|
||||
sx={{
|
||||
justifySelf: "center",
|
||||
@ -85,6 +91,7 @@ export const Header = (props: HeaderProps) => {
|
||||
>
|
||||
<Suspense fallback="search input ">{search}</Suspense>
|
||||
</Box>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
justifySelf: "end",
|
||||
|
Loading…
Reference in New Issue
Block a user