mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-17 17:55:42 +00:00
chore: auf Wiedersehen MUI, hallo Ant Design
This commit is contained in:
parent
a384140aa7
commit
a15c1b2c2f
193
assets/App.tsx
Normal file
193
assets/App.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import {Badge, Layout, Menu, theme} from "antd";
|
||||
import {
|
||||
ApiOutlined,
|
||||
BankOutlined,
|
||||
CloudServerOutlined,
|
||||
FileProtectOutlined,
|
||||
FileSearchOutlined,
|
||||
InfoCircleOutlined,
|
||||
LineChartOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SearchOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import {Navigate, Route, Routes, useNavigate} from "react-router-dom";
|
||||
import TextPage from "./pages/TextPage";
|
||||
import tos from "./content/tos.md";
|
||||
import privacy from "./content/privacy.md";
|
||||
import DomainSearchPage from "./pages/search/DomainSearchPage";
|
||||
import EntitySearchPage from "./pages/search/EntitySearchPage";
|
||||
import NameserverSearchPage from "./pages/search/NameserverSearchPage";
|
||||
import TldPage from "./pages/info/TldPage";
|
||||
import StatisticsPage from "./pages/info/StatisticsPage";
|
||||
import WatchlistsPage from "./pages/tracking/WatchlistsPage";
|
||||
import UserPage from "./pages/UserPage";
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {getUser} from "./utils/api";
|
||||
|
||||
export default function App() {
|
||||
|
||||
const {
|
||||
token: {colorBgContainer, borderRadiusLG},
|
||||
} = theme.useToken()
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const navigate = useNavigate()
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(() => setIsAuthenticated(true)).catch(() => setIsAuthenticated(false))
|
||||
}, []);
|
||||
|
||||
|
||||
return <Layout hasSider style={{minHeight: '100vh'}}>
|
||||
<Layout.Sider>
|
||||
<Menu
|
||||
defaultSelectedKeys={['1-1']}
|
||||
defaultOpenKeys={['1', '2', '3']}
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
items={[
|
||||
{
|
||||
key: '1',
|
||||
label: 'Search',
|
||||
children: [
|
||||
{
|
||||
key: '1-1',
|
||||
icon: <SearchOutlined/>,
|
||||
label: 'Domain',
|
||||
title: 'Domain Finder',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/search/domain')
|
||||
},
|
||||
{
|
||||
key: '1-2',
|
||||
icon: <TeamOutlined/>,
|
||||
label: 'Entity',
|
||||
title: 'Entity Finder',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/search/entity')
|
||||
},
|
||||
{
|
||||
key: '1-3',
|
||||
icon: <CloudServerOutlined/>,
|
||||
label: 'Nameserver',
|
||||
title: 'Nameserver Finder',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/search/nameserver')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
label: 'Information',
|
||||
children: [
|
||||
{
|
||||
key: '2-1',
|
||||
icon: <BankOutlined/>,
|
||||
label: 'TLD',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/info/tld')
|
||||
},
|
||||
{
|
||||
key: '2-2',
|
||||
icon: <LineChartOutlined/>,
|
||||
label: 'Statistics',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/info/stats')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
label: 'Tracking',
|
||||
children: [
|
||||
{
|
||||
key: '3-1',
|
||||
icon: <Badge count={0} size="small"><FileSearchOutlined
|
||||
shape="square"/></Badge>,
|
||||
label: 'My Watchlists',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/tracking/watchlist')
|
||||
},
|
||||
{
|
||||
key: '3-2',
|
||||
icon: <ApiOutlined/>,
|
||||
label: 'My connectors',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/tracking/connectors')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
icon: <UserOutlined/>,
|
||||
label: 'My Account',
|
||||
disabled: !isAuthenticated,
|
||||
onClick: () => navigate('/user')
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
icon: <QuestionCircleOutlined/>,
|
||||
label: 'FAQ',
|
||||
onClick: () => navigate('/faq')
|
||||
},
|
||||
{
|
||||
key: '6',
|
||||
icon: <InfoCircleOutlined/>,
|
||||
label: 'TOS',
|
||||
onClick: () => navigate('/tos')
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
icon: <FileProtectOutlined/>,
|
||||
label: 'Privacy Policy',
|
||||
onClick: () => navigate('/privacy')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<div className="demo-logo-vertical"></div>
|
||||
</Layout.Sider>
|
||||
<Layout>
|
||||
<Layout.Header style={{padding: 0, background: colorBgContainer}}/>
|
||||
<Layout.Content style={{margin: '24px 16px 0'}}>
|
||||
<div style={{
|
||||
padding: 24,
|
||||
minHeight: 360,
|
||||
background: colorBgContainer,
|
||||
borderRadius: borderRadiusLG,
|
||||
}}>
|
||||
|
||||
<Routes>
|
||||
<Route path="/tos" element={<TextPage markdown={tos}/>}/>
|
||||
<Route path="/privacy" element={<TextPage markdown={privacy}/>}/>
|
||||
|
||||
{isAuthenticated ?
|
||||
<>
|
||||
<Route path="/" element={<Navigate to="/search/domain"/>}/>
|
||||
|
||||
<Route path="/search/domain" element={<DomainSearchPage/>}/>
|
||||
<Route path="/search/entity" element={<EntitySearchPage/>}/>
|
||||
<Route path="/search/nameserver" element={<NameserverSearchPage/>}/>
|
||||
|
||||
<Route path="/info/tld" element={<TldPage/>}/>
|
||||
<Route path="/info/stats" element={<StatisticsPage/>}/>
|
||||
|
||||
<Route path="/tracking/watchlist" element={<WatchlistsPage/>}/>
|
||||
|
||||
<Route path="/user" element={<UserPage/>}/>
|
||||
</>
|
||||
:
|
||||
<Route path="*" element={<LoginPage/>}/>
|
||||
}
|
||||
|
||||
</Routes>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
<Layout.Footer style={{textAlign: 'center'}}>
|
||||
Domain Watchdog ©{new Date().getFullYear()} Created by Maël Gangloff
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Container from '@mui/material/Container';
|
||||
import {NavLink} from "react-router-dom";
|
||||
import ToggleColorMode from "./ToggleColorMode";
|
||||
import {PaletteMode} from "@mui/material";
|
||||
import Link from "@mui/material/Link";
|
||||
import {Pets} from "@mui/icons-material";
|
||||
|
||||
interface AppAppBarProps {
|
||||
mode: PaletteMode;
|
||||
toggleColorMode: () => void;
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
export default function AppAppBar({mode, toggleColorMode, isAuthenticated}: AppAppBarProps) {
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{boxShadow: 0, bgcolor: 'transparent', backgroundImage: 'none', mt: 2}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Toolbar
|
||||
variant="regular"
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexShrink: 0,
|
||||
borderRadius: '999px',
|
||||
backdropFilter: 'blur(24px)',
|
||||
maxHeight: 40,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'hsla(220, 60%, 99%, 0.6)',
|
||||
boxShadow:
|
||||
'0 1px 2px hsla(210, 0%, 0%, 0.05), 0 2px 12px hsla(210, 100%, 80%, 0.5)',
|
||||
...theme.applyStyles('dark', {
|
||||
bgcolor: 'hsla(220, 0%, 0%, 0.7)',
|
||||
boxShadow:
|
||||
'0 1px 2px hsla(210, 0%, 0%, 0.5), 0 2px 12px hsla(210, 100%, 25%, 0.3)',
|
||||
}),
|
||||
})}
|
||||
>
|
||||
<Box sx={{flexGrow: 1, display: 'flex', alignItems: 'center', px: 0}}>
|
||||
<Pets color="secondary"/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: {xs: 'none', md: 'flex'},
|
||||
gap: 0.5,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ToggleColorMode
|
||||
data-screenshot="toggle-mode"
|
||||
mode={mode}
|
||||
toggleColorMode={toggleColorMode}
|
||||
/>
|
||||
{
|
||||
!isAuthenticated ?
|
||||
<NavLink to="/login">
|
||||
<Button color="primary" variant="text" size="small">
|
||||
Sign in
|
||||
</Button>
|
||||
</NavLink>
|
||||
: <Link href="/logout">
|
||||
<Button color="primary" variant="text" size="small">
|
||||
Log out
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
)
|
||||
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import {Bookmark, ChevronLeft, Dns, Explore, LocalPolice, MenuBook, People} from "@mui/icons-material";
|
||||
import {Divider, List, ListItemButton, ListItemIcon, ListItemText, ListSubheader, styled} from "@mui/material";
|
||||
import React from "react";
|
||||
import MuiDrawer from "@mui/material/Drawer";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
|
||||
const Drawer = styled(MuiDrawer, {shouldForwardProp: (prop) => prop !== 'open'})(
|
||||
({theme, open}) => ({
|
||||
'& .MuiDrawer-paper': {
|
||||
position: 'relative',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 240,
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
boxSizing: 'border-box',
|
||||
...(!open && {
|
||||
overflowX: 'hidden',
|
||||
transition: theme.transitions.create('width', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen,
|
||||
}),
|
||||
width: theme.spacing(7),
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
width: theme.spacing(9),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export default function DrawerBox() {
|
||||
const [open, setOpen] = React.useState(true);
|
||||
const toggleDrawer = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
const navigate = useNavigate()
|
||||
|
||||
return <Drawer variant="permanent" open={open}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
mt: 5,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
px: [1],
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={toggleDrawer}>
|
||||
<ChevronLeft/>
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
<Divider/>
|
||||
<List component="nav">
|
||||
<ListSubheader component="div" inset>
|
||||
Domain names
|
||||
</ListSubheader>
|
||||
<ListItemButton onClick={() => navigate('/finder/domain')}>
|
||||
<ListItemIcon>
|
||||
<Explore/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Domain finder"/>
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => navigate('/tld')}>
|
||||
<ListItemIcon>
|
||||
<LocalPolice/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Top Level Domain"/>
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => navigate('/finder/nameserver')}>
|
||||
<ListItemIcon>
|
||||
<Dns/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Nameserver"/>
|
||||
</ListItemButton>
|
||||
|
||||
<Divider sx={{my: 1}}/>
|
||||
<ListSubheader component="div" inset>
|
||||
Entities
|
||||
</ListSubheader>
|
||||
<ListItemButton onClick={() => navigate('/finder/entity')}>
|
||||
<ListItemIcon>
|
||||
<People/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Entity finder"/>
|
||||
</ListItemButton>
|
||||
<ListItemButton onClick={() => navigate('/reverse')}>
|
||||
<ListItemIcon>
|
||||
<MenuBook/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Reverse directory"/>
|
||||
</ListItemButton>
|
||||
|
||||
<Divider sx={{my: 1}}/>
|
||||
<ListSubheader component="div" inset>
|
||||
Tracking
|
||||
</ListSubheader>
|
||||
<ListItemButton onClick={() => navigate('/watchlist')}>
|
||||
<ListItemIcon>
|
||||
<Bookmark/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="My watchlists"/>
|
||||
</ListItemButton>
|
||||
</List>
|
||||
</Drawer>
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Container from '@mui/material/Container';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import {NavLink} from "react-router-dom";
|
||||
|
||||
function Copyright() {
|
||||
return (
|
||||
<Typography variant="body2" sx={{color: 'text.secondary', mt: 1}}>
|
||||
{'Copyright © '}
|
||||
<Link href="https://github.com/maelgangloff/domain-watchdog">Domain Watchdog </Link>
|
||||
{new Date().getFullYear()}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: {xs: 4, sm: 8},
|
||||
py: {xs: 8, sm: 10},
|
||||
textAlign: {sm: 'center', md: 'left'},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
pt: {xs: 4, sm: 8},
|
||||
width: '100%',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<NavLink to="/privacy">
|
||||
<Link color="text.secondary" variant="body2">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</NavLink>
|
||||
<Typography sx={{display: 'inline', mx: 0.5, opacity: 0.5}}>
|
||||
•
|
||||
</Typography>
|
||||
<NavLink to="/tos">
|
||||
<Link color="text.secondary" variant="body2">
|
||||
Terms of Service
|
||||
</Link>
|
||||
</NavLink>
|
||||
<Copyright/>
|
||||
</div>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
useFlexGap
|
||||
sx={{justifyContent: 'left', color: 'text.secondary'}}
|
||||
>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
href="https://github.com/maelgangloff/domain-watchdog"
|
||||
aria-label="GitHub"
|
||||
sx={{alignSelf: 'center'}}
|
||||
>
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import React from "react";
|
||||
import {Paper, Table, TableBody, TableCell, TableContainer, TableHead, TablePagination, TableRow} from "@mui/material";
|
||||
|
||||
interface Column {
|
||||
id: string;
|
||||
label: string;
|
||||
minWidth?: number;
|
||||
align?: 'right';
|
||||
format?: (value: any) => any;
|
||||
}
|
||||
|
||||
export default function HeadTable({columns, rows}: { rows: any[], columns: Column[] }) {
|
||||
const [page, setPage] = React.useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
||||
|
||||
const handleChangePage = (event: unknown, newPage: number) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setRowsPerPage(+event.target.value);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{width: '100%', overflow: 'hidden'}}>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
align={column.align}
|
||||
style={{minWidth: column.minWidth}}
|
||||
>
|
||||
{column.label}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows
|
||||
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
||||
.map((row: any) => {
|
||||
return (
|
||||
<TableRow hover sx={{'&:last-child td, &:last-child th': {border: 0}}}
|
||||
role="checkbox" tabIndex={-1} key={row.code}>
|
||||
{columns.map((column) => {
|
||||
const value = row[column.id as keyof typeof row]
|
||||
return (
|
||||
<TableCell key={column.id} align={column.align}>
|
||||
{column.format ? column.format(value) : value}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[10, 25, 100]}
|
||||
component="div"
|
||||
count={rows.length}
|
||||
rowsPerPage={rowsPerPage}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
0
assets/components/Sider.tsx
Normal file
0
assets/components/Sider.tsx
Normal file
@ -1,33 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import {PaletteMode} from '@mui/material';
|
||||
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
|
||||
|
||||
import WbSunnyRoundedIcon from '@mui/icons-material/WbSunnyRounded';
|
||||
import ModeNightRoundedIcon from '@mui/icons-material/ModeNightRounded';
|
||||
|
||||
interface ToggleColorModeProps extends IconButtonProps {
|
||||
mode: PaletteMode;
|
||||
toggleColorMode: () => void;
|
||||
}
|
||||
|
||||
export default function ToggleColorMode({
|
||||
mode,
|
||||
toggleColorMode,
|
||||
...props
|
||||
}: ToggleColorModeProps) {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
color="primary"
|
||||
aria-label="Theme toggle button"
|
||||
size="small"
|
||||
{...props}
|
||||
>
|
||||
{mode === 'dark' ? (
|
||||
<WbSunnyRoundedIcon fontSize="small"/>
|
||||
) : (
|
||||
<ModeNightRoundedIcon fontSize="small"/>
|
||||
)}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
2
assets/content/.gitignore
vendored
2
assets/content/.gitignore
vendored
@ -1 +1 @@
|
||||
*.md
|
||||
*.md
|
||||
|
||||
1
assets/content/privacy.md
Normal file
1
assets/content/privacy.md
Normal file
@ -0,0 +1 @@
|
||||
# Privacy Policy
|
||||
1
assets/content/tos.md
Normal file
1
assets/content/tos.md
Normal file
@ -0,0 +1 @@
|
||||
# Terms of Service
|
||||
3
assets/index.css
Normal file
3
assets/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
@ -1,70 +1,21 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import TextPage from "./pages/TextPage";
|
||||
import {HashRouter, Navigate, Route, Routes} from "react-router-dom";
|
||||
import App from "./App";
|
||||
import {HashRouter} from "react-router-dom";
|
||||
|
||||
import tosContent from "./content/tos.md"
|
||||
import privacyContent from "./content/privacy.md"
|
||||
import LoginPage from "./pages/LoginPage";
|
||||
import {createTheme, PaletteMode, ThemeProvider} from "@mui/material";
|
||||
import CssBaseline from "@mui/material/CssBaseline";
|
||||
import AppAppBar from "./components/AppAppBar";
|
||||
import {getUser} from "./utils/api/user";
|
||||
import DrawerBox from "./components/DrawerBox";
|
||||
import Box from "@mui/material/Box";
|
||||
import DomainFinderPage from "./pages/DomainFinderPage";
|
||||
import EntityFinderPage from "./pages/EntityFinderPage";
|
||||
import NameserverFinderPage from "./pages/NameserverFinderPage";
|
||||
import ReverseDirectoryPage from "./pages/ReverseDirectoryPage";
|
||||
import TldPage from "./pages/TldPage";
|
||||
import WatchlistsPage from "./pages/WatchlistsPage";
|
||||
import './index.css'
|
||||
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
|
||||
function Index() {
|
||||
|
||||
function App() {
|
||||
const [mode, setMode] = React.useState<PaletteMode>('dark')
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
return (
|
||||
<HashRouter>
|
||||
<App/>
|
||||
</HashRouter>
|
||||
|
||||
const toggleColorMode = () => {
|
||||
setMode((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(() => setIsAuthenticated(true)).catch(() => setIsAuthenticated(false))
|
||||
}, []);
|
||||
|
||||
return <React.StrictMode>
|
||||
<ThemeProvider theme={createTheme({palette: {mode: mode}})}>
|
||||
<HashRouter>
|
||||
<Box sx={{display: 'flex'}}>
|
||||
<CssBaseline/>
|
||||
{isAuthenticated && <DrawerBox/>}
|
||||
|
||||
<AppAppBar mode={mode} toggleColorMode={toggleColorMode} isAuthenticated={isAuthenticated}/>
|
||||
<Routes>
|
||||
{isAuthenticated ?
|
||||
<>
|
||||
<Route path="/" element={<Navigate to="/finder/domain"/>}/>
|
||||
<Route path="/finder/domain" element={<DomainFinderPage/>}/>
|
||||
<Route path="/finder/entity" element={<EntityFinderPage/>}/>
|
||||
<Route path="/finder/nameserver" element={<NameserverFinderPage/>}/>
|
||||
<Route path="/reverse" element={<ReverseDirectoryPage/>}/>
|
||||
<Route path="/tld" element={<TldPage/>}/>
|
||||
<Route path="/watchlist" element={<WatchlistsPage/>}/>
|
||||
</>
|
||||
:
|
||||
<Route path="*" element={<LoginPage setIsAuthenticated={setIsAuthenticated}/>}/>
|
||||
}
|
||||
<Route path="/tos" element={<TextPage content={tosContent}/>}/>
|
||||
<Route path="/privacy" element={<TextPage content={privacyContent}/>}/>
|
||||
|
||||
</Routes>
|
||||
</Box>
|
||||
</HashRouter>
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
root.render(<App/>)
|
||||
root.render(<Index/>)
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
import React, {ChangeEvent, useState} from 'react';
|
||||
import Container from "@mui/material/Container";
|
||||
import {Grid, InputAdornment, Paper} from "@mui/material";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {Explore} from "@mui/icons-material";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
|
||||
export default function DomainFinderPage() {
|
||||
const [ldhName, setLdhName] = useState("")
|
||||
const [error, setError] = useState(false)
|
||||
|
||||
const onChangeDomain = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setLdhName(e.currentTarget.value);
|
||||
const regex = /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/
|
||||
setError(!regex.test(e.currentTarget.value))
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container maxWidth="lg" sx={{mt: 20, mb: 4}}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8} lg={9}>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 2,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 240,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
sx={{mt: 5}} label="Domain name" variant="standard" value={ldhName}
|
||||
onChange={onChangeDomain}
|
||||
helperText={error && "This domain name does not appear to be valid"}
|
||||
error={error}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<Explore/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant="subtitle2" sx={{mt: 3}}>
|
||||
This tool allows you to search for a domain name in the database.
|
||||
As a reminder, if a domain name is unknown to Domain Watchdog or if the data is
|
||||
more
|
||||
than a week old, an RDAP search will be performed. The RDAP search is an operation worth
|
||||
a token.
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Footer/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import Container from "@mui/material/Container";
|
||||
import {Grid} from "@mui/material";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
|
||||
export default function EntityFinderPage() {
|
||||
return (
|
||||
<>
|
||||
<Container maxWidth="lg" sx={{mt: 20, mb: 4}}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8} lg={9}>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Footer/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,110 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import {useState} from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Box from '@mui/material/Box';
|
||||
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Link from "@mui/material/Link";
|
||||
import {login} from "../utils/api";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import {Alert} from "@mui/material";
|
||||
import Container from "@mui/material/Container";
|
||||
import Footer from "../components/Footer";
|
||||
import React from "react";
|
||||
|
||||
interface Props {
|
||||
setIsAuthenticated: (val: boolean) => void
|
||||
}
|
||||
|
||||
export default function LoginPage({setIsAuthenticated}: Props) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [error, setError] = useState<string>('')
|
||||
const [credentials, setCredentials] = useState({email: "", password: ""})
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await login(credentials.email, credentials.password);
|
||||
setIsAuthenticated(true)
|
||||
navigate('/');
|
||||
|
||||
} catch (e: any) {
|
||||
setCredentials({...credentials, password: ""})
|
||||
setError(e.response.data.message)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container component="main" maxWidth="xs">
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 20,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Avatar sx={{m: 1, bgcolor: 'secondary.main'}}>
|
||||
<LockOutlinedIcon/>
|
||||
</Avatar>
|
||||
<Typography component="h1" variant="h5">
|
||||
Sign in
|
||||
</Typography>
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1}}>
|
||||
{
|
||||
error !== "" && <Alert variant="outlined" severity="error">{error}</Alert>
|
||||
}
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
value={credentials.email}
|
||||
onChange={(e) => setCredentials({...credentials, email: e.currentTarget.value})}
|
||||
/>
|
||||
<TextField
|
||||
margin="normal"
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
value={credentials.password}
|
||||
autoComplete="current-password"
|
||||
onChange={(e) => setCredentials({...credentials, password: e.currentTarget.value})}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
sx={{mt: 3}}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
<Link href="/login/oauth">
|
||||
<Button
|
||||
type="button"
|
||||
fullWidth
|
||||
color='secondary'
|
||||
variant="contained"
|
||||
sx={{mt: 3, mb: 2}}
|
||||
>
|
||||
Single Sign-On
|
||||
</Button>
|
||||
</Link>
|
||||
<Footer/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
export default function Page() {
|
||||
return <p>
|
||||
Login Page
|
||||
</p>
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import Container from "@mui/material/Container";
|
||||
import {Grid} from "@mui/material";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
|
||||
export default function NameserverFinderPage() {
|
||||
return (
|
||||
<>
|
||||
<Container maxWidth="lg" sx={{mt: 20, mb: 4}}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8} lg={9}>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Footer/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
12
assets/pages/NotFoundPage.tsx
Normal file
12
assets/pages/NotFoundPage.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import {Button, Result} from "antd";
|
||||
import React from "react";
|
||||
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return <Result
|
||||
status="404"
|
||||
title="404"
|
||||
subTitle="Sorry, the page you visited does not exist."
|
||||
extra={<Button type="primary">Back Home</Button>}
|
||||
/>
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import Container from "@mui/material/Container";
|
||||
import {Grid} from "@mui/material";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
|
||||
export default function ReverseDirectoryPage() {
|
||||
return (
|
||||
<>
|
||||
<Container maxWidth="lg" sx={{mt: 20, mb: 4}}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8} lg={9}>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Footer/>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,38 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Container from "@mui/material/Container";
|
||||
import Footer from "../components/Footer";
|
||||
import React from "react";
|
||||
import snarkdown from "snarkdown"
|
||||
|
||||
interface Props {
|
||||
content: string
|
||||
}
|
||||
|
||||
export default function Index({content}: Props) {
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: {xs: 4, sm: 8},
|
||||
py: {xs: 8, sm: 10},
|
||||
textAlign: {sm: 'center', md: 'left'},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
pt: {xs: 4, sm: 8},
|
||||
width: '100%',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{__html: content}}></div>
|
||||
</Box>
|
||||
<Footer/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
}
|
||||
export default function TextPage({markdown}: { markdown: string }) {
|
||||
return <div dangerouslySetInnerHTML={{__html: snarkdown(markdown)}}></div>
|
||||
}
|
||||
@ -1,110 +0,0 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import Container from "@mui/material/Container";
|
||||
import {Accordion, AccordionDetails, AccordionSummary, Grid, Typography} from "@mui/material";
|
||||
import {ExpandMore} from "@mui/icons-material";
|
||||
import HeadTable from "../components/HeadTable";
|
||||
import {getTldList} from "../utils/api";
|
||||
import Footer from "../components/Footer";
|
||||
|
||||
const gTldColumns = [
|
||||
{id: 'tld', label: 'TLD'},
|
||||
{id: 'registryOperator', label: 'Operator'}
|
||||
]
|
||||
|
||||
const sTldColumns = [
|
||||
{id: 'tld', label: 'TLD'}
|
||||
]
|
||||
|
||||
const toEmoji = (tld: string) => String.fromCodePoint(
|
||||
...getCountryCode(tld)
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.map((char) => 127397 + char.charCodeAt(0)
|
||||
)
|
||||
)
|
||||
|
||||
const getCountryCode = (tld: string): string => {
|
||||
const exceptions = {uk: 'gb', su: 'ru', tp: 'tl'}
|
||||
if (tld in exceptions) return exceptions[tld as keyof typeof exceptions]
|
||||
return tld
|
||||
}
|
||||
|
||||
const regionNames = new Intl.DisplayNames(['en'], {type: 'region'})
|
||||
|
||||
const ccTldColumns = [
|
||||
{id: 'tld', label: 'TLD'},
|
||||
{
|
||||
id: 'tld',
|
||||
label: 'Flag',
|
||||
format: (tld: string) => toEmoji(tld)
|
||||
},
|
||||
{id: 'tld', label: 'Country name', format: (tld: string) => regionNames.of(getCountryCode(tld)) ?? '-'},
|
||||
]
|
||||
|
||||
export default function TldPage() {
|
||||
const [sTld, setSTld] = useState<any>([])
|
||||
const [gTld, setGTld] = useState<any>([])
|
||||
const [ccTld, setCcTld] = useState<any>([])
|
||||
const [brandGTld, setBrandGTld] = useState<any>([])
|
||||
|
||||
useEffect(() => {
|
||||
getTldList({type: 'sTLD'}).then(setSTld)
|
||||
getTldList({type: 'gTLD', contractTerminated: 0, specification13: 0}).then(setGTld)
|
||||
getTldList({type: 'gTLD', contractTerminated: 0, specification13: 1}).then(setBrandGTld)
|
||||
getTldList({type: 'ccTLD'}).then(setCcTld)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{mt: 20, mb: 4}}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8} lg={9}>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore/>}>
|
||||
<Typography sx={{width: '33%', flexShrink: 0}}>
|
||||
sTLD
|
||||
</Typography>
|
||||
<Typography sx={{color: 'text.secondary'}}>Sponsored Top-Level Domains</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<HeadTable rows={sTld} columns={sTldColumns}/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore/>}>
|
||||
<Typography sx={{width: '33%', flexShrink: 0}}>
|
||||
gTLD
|
||||
</Typography>
|
||||
<Typography sx={{color: 'text.secondary'}}>Generic Top-Level Domains</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<HeadTable rows={gTld} columns={gTldColumns}/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore/>}>
|
||||
<Typography sx={{width: '33%', flexShrink: 0}}>
|
||||
Brand gTLD
|
||||
</Typography>
|
||||
<Typography sx={{color: 'text.secondary'}}>Brand Generic Top-Level Domains</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<HeadTable rows={brandGTld} columns={gTldColumns}/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMore/>}>
|
||||
<Typography sx={{width: '33%', flexShrink: 0}}>
|
||||
ccTLD
|
||||
</Typography>
|
||||
<Typography sx={{color: 'text.secondary'}}>Country-Code Top-Level Domains</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<HeadTable rows={ccTld} columns={ccTldColumns}/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Footer/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
7
assets/pages/UserPage.tsx
Normal file
7
assets/pages/UserPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Page() {
|
||||
return <p>
|
||||
Hey
|
||||
</p>
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import Container from "@mui/material/Container";
|
||||
import {Grid, List, ListItem, ListItemText} from "@mui/material";
|
||||
import Footer from "../components/Footer";
|
||||
import {deleteWatchlist, getWatchlists, Watchlist} from "../utils/api";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import {DeleteForever} from '@mui/icons-material'
|
||||
|
||||
export default function WatchlistsPage() {
|
||||
const [watchlists, setWatchlists] = useState<(Partial<Watchlist> & { token: string })[]>([])
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
getWatchlists().then(setWatchlists)
|
||||
}, [refreshKey])
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{mt: 20, mb: 4}}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item xs={12} md={8} lg={9}>
|
||||
<List sx={{width: '100%', bgcolor: 'background.paper'}}>
|
||||
{watchlists.map((w) => (
|
||||
<ListItem
|
||||
key={w.token}
|
||||
secondaryAction={
|
||||
<IconButton aria-label="delete"
|
||||
onClick={(e) => deleteWatchlist(w.token).then(() => setRefreshKey(refreshKey + 1))}>
|
||||
<DeleteForever/>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText primary={`Token ${w.token}`}/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Footer/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
7
assets/pages/info/StatisticsPage.tsx
Normal file
7
assets/pages/info/StatisticsPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function StatisticsPage() {
|
||||
return <p>
|
||||
Tld
|
||||
</p>
|
||||
}
|
||||
7
assets/pages/info/TldPage.tsx
Normal file
7
assets/pages/info/TldPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function TldPage() {
|
||||
return <p>
|
||||
Tld
|
||||
</p>
|
||||
}
|
||||
7
assets/pages/search/DomainSearchPage.tsx
Normal file
7
assets/pages/search/DomainSearchPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function DomainSearchPage() {
|
||||
return <p>
|
||||
|
||||
</p>
|
||||
}
|
||||
7
assets/pages/search/EntitySearchPage.tsx
Normal file
7
assets/pages/search/EntitySearchPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function EntitySearchPage() {
|
||||
return <p>
|
||||
|
||||
</p>
|
||||
}
|
||||
7
assets/pages/search/NameserverSearchPage.tsx
Normal file
7
assets/pages/search/NameserverSearchPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function NameserverSearchPage() {
|
||||
return <p>
|
||||
NS Finder
|
||||
</p>
|
||||
}
|
||||
7
assets/pages/tracking/ConnectorsPage.tsx
Normal file
7
assets/pages/tracking/ConnectorsPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function ConnectorsPage() {
|
||||
return <p>
|
||||
|
||||
</p>
|
||||
}
|
||||
7
assets/pages/tracking/WatchlistsPage.tsx
Normal file
7
assets/pages/tracking/WatchlistsPage.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
export default function WatchlistsPage() {
|
||||
return <p>
|
||||
|
||||
</p>
|
||||
}
|
||||
@ -18,24 +18,21 @@
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@mui/icons-material": "^5.16.4",
|
||||
"@mui/material": "^5.16.4",
|
||||
"@symfony/webpack-encore": "^4.0.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/jsonld": "^1.5.15",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"antd": "^5.19.3",
|
||||
"axios": "^1.7.2",
|
||||
"core-js": "^3.23.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"html-loader": "^5.1.0",
|
||||
"jsonld": "^8.3.2",
|
||||
"markdown-loader": "^8.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"snarkdown": "^2.0.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.5.3",
|
||||
"webpack": "^5.74.0",
|
||||
|
||||
@ -67,10 +67,7 @@ Encore
|
||||
use: [
|
||||
{
|
||||
loader: "html-loader",
|
||||
},
|
||||
{
|
||||
loader: "markdown-loader"
|
||||
},
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user