mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-17 17:55:42 +00:00
feat: add frontend
This commit is contained in:
parent
1642767993
commit
3d7a6fbcfd
148
assets/components/AppAppBar.tsx
Normal file
148
assets/components/AppAppBar.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {PaletteMode} from '@mui/material';
|
||||||
|
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 IconButton from '@mui/material/IconButton';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import Drawer from '@mui/material/Drawer';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||||
|
import ToggleColorMode from './ToggleColorMode';
|
||||||
|
|
||||||
|
import Sitemark from './SitemarkIcon';
|
||||||
|
import {NavLink} from "react-router-dom";
|
||||||
|
|
||||||
|
interface AppAppBarProps {
|
||||||
|
mode: PaletteMode;
|
||||||
|
toggleColorMode: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppAppBar({mode, toggleColorMode}: AppAppBarProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const toggleDrawer = (newOpen: boolean) => () => {
|
||||||
|
setOpen(newOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToSection = (sectionId: string) => {
|
||||||
|
const sectionElement = document.getElementById(sectionId);
|
||||||
|
const offset = 128;
|
||||||
|
if (sectionElement) {
|
||||||
|
const targetScroll = sectionElement.offsetTop - offset;
|
||||||
|
sectionElement.scrollIntoView({behavior: 'smooth'});
|
||||||
|
window.scrollTo({
|
||||||
|
top: targetScroll,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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}}>
|
||||||
|
<Sitemark/>
|
||||||
|
<Box sx={{display: {xs: 'none', md: 'flex'}}}>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="info"
|
||||||
|
size="small"
|
||||||
|
onClick={() => scrollToSection('highlights')}
|
||||||
|
>
|
||||||
|
Highlights
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="info"
|
||||||
|
size="small"
|
||||||
|
onClick={() => scrollToSection('faq')}
|
||||||
|
sx={{minWidth: 0}}
|
||||||
|
>
|
||||||
|
FAQ
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: {xs: 'none', md: 'flex'},
|
||||||
|
gap: 0.5,
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleColorMode
|
||||||
|
data-screenshot="toggle-mode"
|
||||||
|
mode={mode}
|
||||||
|
toggleColorMode={toggleColorMode}
|
||||||
|
/>
|
||||||
|
<NavLink to="/login">
|
||||||
|
<Button color="primary" variant="text" size="small">
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</NavLink>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{display: {sm: 'flex', md: 'none'}}}>
|
||||||
|
<IconButton aria-label="Menu button" onClick={toggleDrawer(true)}>
|
||||||
|
<MenuIcon/>
|
||||||
|
</IconButton>
|
||||||
|
<Drawer anchor="top" open={open} onClose={toggleDrawer(false)}>
|
||||||
|
<Box sx={{p: 2, backgroundColor: 'background.default'}}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleColorMode mode={mode} toggleColorMode={toggleColorMode}/>
|
||||||
|
<IconButton onClick={toggleDrawer(false)}>
|
||||||
|
<CloseRoundedIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
<Divider sx={{my: 3}}/>
|
||||||
|
<MenuItem onClick={() => scrollToSection('highlights')}>
|
||||||
|
Highlights
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => scrollToSection('faq')}>FAQ</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<Button color="primary" variant="outlined" fullWidth>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</MenuItem>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
</Box>
|
||||||
|
</Toolbar>
|
||||||
|
</Container>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
}
|
||||||
183
assets/components/FAQ.tsx
Normal file
183
assets/components/FAQ.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Accordion from '@mui/material/Accordion';
|
||||||
|
import AccordionDetails from '@mui/material/AccordionDetails';
|
||||||
|
import AccordionSummary from '@mui/material/AccordionSummary';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
|
||||||
|
export default function FAQ() {
|
||||||
|
const [expanded, setExpanded] = React.useState<string | false>(false);
|
||||||
|
|
||||||
|
const handleChange =
|
||||||
|
(panel: string) => (event: React.SyntheticEvent, isExpanded: boolean) => {
|
||||||
|
setExpanded(isExpanded ? panel : false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
id="faq"
|
||||||
|
sx={{
|
||||||
|
pt: {xs: 4, sm: 12},
|
||||||
|
pb: {xs: 8, sm: 16},
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: {xs: 3, sm: 6},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
component="h2"
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
color: 'text.primary',
|
||||||
|
width: {sm: '100%', md: '60%'},
|
||||||
|
textAlign: {sm: 'left', md: 'center'},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Frequently asked questions
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{width: '100%'}}>
|
||||||
|
<Accordion
|
||||||
|
expanded={expanded === 'panel1'}
|
||||||
|
onChange={handleChange('panel1')}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel1d-content"
|
||||||
|
id="panel1d-header"
|
||||||
|
>
|
||||||
|
<Typography component="h3" variant="subtitle2">
|
||||||
|
May I reuse the data obtained from Domain Watchdog?
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{maxWidth: {sm: '100%', md: '70%'}}}
|
||||||
|
>
|
||||||
|
Although the source code of this project is open source, the license does not
|
||||||
|
extend to the data collected by it.<br/>
|
||||||
|
This data is redistributed under the same conditions as when it was obtained.
|
||||||
|
This means that you must respect the reuse conditions of each of the RDAP servers used.<br/>
|
||||||
|
<br/>
|
||||||
|
For each domain, Domain Watchdog tells you which RDAP server was contacted. <b>It is your
|
||||||
|
responsibility to check the conditions of use of this server.</b>
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
expanded={expanded === 'panel2'}
|
||||||
|
onChange={handleChange('panel2')}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel2d-content"
|
||||||
|
id="panel2d-header"
|
||||||
|
>
|
||||||
|
<Typography component="h3" variant="subtitle2">
|
||||||
|
What is an RDAP server?
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{maxWidth: {sm: '100%', md: '70%'}}}
|
||||||
|
>
|
||||||
|
The latest version of the WHOIS protocol was standardized in 2004 by RFC 3912 This
|
||||||
|
protocol allows anyone to retrieve key information concerning a domain name, an IP address,
|
||||||
|
or an entity registered with a registry.<br/>
|
||||||
|
<br/>
|
||||||
|
ICANN launched a global vote in 2023 to propose replacing the WHOIS protocol with RDAP. As a
|
||||||
|
result, registries and registrars will no longer be required to support WHOIS from 2025
|
||||||
|
(WHOIS Sunset Date).<br/>
|
||||||
|
<br/>
|
||||||
|
Domain Watchdog uses the RDAP protocol, which will soon be the new standard for retrieving
|
||||||
|
information concerning domain names.
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
expanded={expanded === 'panel3'}
|
||||||
|
onChange={handleChange('panel3')}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel3d-content"
|
||||||
|
id="panel3d-header"
|
||||||
|
>
|
||||||
|
<Typography component="h3" variant="subtitle2">
|
||||||
|
What are Domain Watchdog's data sources?
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{maxWidth: {sm: '100%', md: '70%'}}}
|
||||||
|
>
|
||||||
|
This project relies on open access data.
|
||||||
|
Domain Watchdog uses the RDAP protocol, which will soon be the new standard for retrieving
|
||||||
|
information concerning domain names.
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
expanded={expanded === 'panel4'}
|
||||||
|
onChange={handleChange('panel4')}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel4d-content"
|
||||||
|
id="panel4d-header"
|
||||||
|
>
|
||||||
|
<Typography component="h3" variant="subtitle2">
|
||||||
|
What is the added value of Domain Watchdog rather than doing RDAP queries yourself?
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{maxWidth: {sm: '100%', md: '70%'}}}
|
||||||
|
>
|
||||||
|
Although the RDAP and WHOIS protocols allow you to obtain precise information about a
|
||||||
|
domain, it is not possible to perform a reverse search to discover a list of domain names
|
||||||
|
associated with an entity. Additionally, accessing a detailed history of events (ownership
|
||||||
|
changes, renewals, etc.) is not feasible with these protocols.
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
<Accordion
|
||||||
|
expanded={expanded === 'panel5'}
|
||||||
|
onChange={handleChange('panel5')}
|
||||||
|
>
|
||||||
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon/>}
|
||||||
|
aria-controls="panel5d-content"
|
||||||
|
id="panel5d-header"
|
||||||
|
>
|
||||||
|
<Typography component="h3" variant="subtitle2">
|
||||||
|
Under what license is the source code for this project released?
|
||||||
|
</Typography>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
gutterBottom
|
||||||
|
sx={{maxWidth: {sm: '100%', md: '70%'}}}
|
||||||
|
>
|
||||||
|
This entire project is licensed under GNU Affero General Public License v3.0 or later.
|
||||||
|
The source code is published on GitHub and freely accessible.
|
||||||
|
</Typography>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
assets/components/Footer.tsx
Normal file
73
assets/components/Footer.tsx
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
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 FacebookIcon from '@mui/icons-material/GitHub';
|
||||||
|
|
||||||
|
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>
|
||||||
|
<Link color="text.secondary" variant="body2" href="#">
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
<Typography sx={{display: 'inline', mx: 0.5, opacity: 0.5}}>
|
||||||
|
•
|
||||||
|
</Typography>
|
||||||
|
<Link color="text.secondary" variant="body2" href="#">
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
<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'}}
|
||||||
|
>
|
||||||
|
<FacebookIcon/>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
assets/components/Hero.tsx
Normal file
97
assets/components/Hero.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import {styled} from '@mui/material/styles';
|
||||||
|
|
||||||
|
const StyledBox = styled('div')(({theme}) => ({
|
||||||
|
alignSelf: 'center',
|
||||||
|
width: '100%',
|
||||||
|
height: 400,
|
||||||
|
marginTop: theme.spacing(8),
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
outline: '1px solid',
|
||||||
|
boxShadow: '0 0 12px 8px hsla(220, 25%, 80%, 0.2)',
|
||||||
|
backgroundImage: `url(${'/static/images/templates/templates-images/hero-light.png'})`,
|
||||||
|
outlineColor: 'hsla(220, 25%, 80%, 0.5)',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
marginTop: theme.spacing(10),
|
||||||
|
height: 700,
|
||||||
|
},
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
boxShadow: '0 0 24px 12px hsla(210, 100%, 25%, 0.2)',
|
||||||
|
backgroundImage: `url(${'/static/images/templates/templates-images/hero-dark.png'})`,
|
||||||
|
outlineColor: 'hsla(210, 100%, 80%, 0.1)',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function Hero() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
id="hero"
|
||||||
|
sx={(theme) => ({
|
||||||
|
width: '100%',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(ellipse 80% 50% at 50% -20%, hsl(210, 100%, 90%), transparent)',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
backgroundImage:
|
||||||
|
'radial-gradient(ellipse 80% 50% at 50% -20%, hsl(210, 100%, 16%), transparent)',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
pt: {xs: 14, sm: 20},
|
||||||
|
pb: {xs: 8, sm: 12},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
spacing={2}
|
||||||
|
useFlexGap
|
||||||
|
sx={{alignItems: 'center', width: {xs: '100%', sm: '70%'}}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: {xs: 'column', sm: 'row'},
|
||||||
|
alignItems: 'center',
|
||||||
|
fontSize: 'clamp(3rem, 10vw, 3.5rem)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Domain
|
||||||
|
<Typography
|
||||||
|
component="span"
|
||||||
|
variant="h1"
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontSize: 'inherit',
|
||||||
|
color: 'primary.main',
|
||||||
|
...theme.applyStyles('dark', {
|
||||||
|
color: 'primary.light',
|
||||||
|
}),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Watchdog
|
||||||
|
</Typography>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
textAlign: 'center',
|
||||||
|
color: 'text.secondary',
|
||||||
|
width: {sm: '100%', md: '80%'},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Explore the fascinating history of domain names with Domain Watchdog.
|
||||||
|
This service collects open access information about domain names, helping track changes.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
assets/components/Highlights.tsx
Normal file
108
assets/components/Highlights.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Card from '@mui/material/Card';
|
||||||
|
import Container from '@mui/material/Container';
|
||||||
|
import Grid from '@mui/material/Grid';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import AutoFixHighRoundedIcon from '@mui/icons-material/AutoFixHighRounded';
|
||||||
|
import QueryStatsRoundedIcon from '@mui/icons-material/QueryStatsRounded';
|
||||||
|
import SettingsSuggestRoundedIcon from '@mui/icons-material/SettingsSuggestRounded';
|
||||||
|
import ThumbUpAltRoundedIcon from '@mui/icons-material/ThumbUpAltRounded';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
icon: <SettingsSuggestRoundedIcon/>,
|
||||||
|
title: 'Virtuous RDAP requests',
|
||||||
|
description:
|
||||||
|
'Domain Watchdog is designed to make as few RDAP requests as possible so as not to overload them.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ThumbUpAltRoundedIcon/>,
|
||||||
|
title: 'Open access API',
|
||||||
|
description:
|
||||||
|
'The Domain Watchdog API is accessible to all its users.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <AutoFixHighRoundedIcon/>,
|
||||||
|
title: 'Open Source',
|
||||||
|
description:
|
||||||
|
'The project is licensed under AGPL-3.0. The source code is freely available on GitHub.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <QueryStatsRoundedIcon/>,
|
||||||
|
title: 'Data quality',
|
||||||
|
description:
|
||||||
|
'The data is retrieved from official top-level domain name registries. Once collected, this data is made available to users of this service.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Highlights() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
id="highlights"
|
||||||
|
sx={{
|
||||||
|
pt: {xs: 4, sm: 12},
|
||||||
|
pb: {xs: 8, sm: 16},
|
||||||
|
color: 'white',
|
||||||
|
bgcolor: 'hsl(220, 30%, 2%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: {xs: 3, sm: 6},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: {sm: '100%', md: '60%'},
|
||||||
|
textAlign: {sm: 'left', md: 'center'},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography component="h2" variant="h4">
|
||||||
|
Highlights
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{color: 'grey.400'}}>
|
||||||
|
Here are the reasons why Domain Watchdog is the solution for domain name tracking.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<Grid item xs={6} sm={6} md={6} key={index}>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
component={Card}
|
||||||
|
spacing={1}
|
||||||
|
useFlexGap
|
||||||
|
sx={{
|
||||||
|
color: 'inherit',
|
||||||
|
p: 3,
|
||||||
|
height: '100%',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'hsla(220, 25%, 25%, .3)',
|
||||||
|
background: 'transparent',
|
||||||
|
backgroundColor: 'grey.900',
|
||||||
|
boxShadow: 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{opacity: '50%'}}>{item.icon}</Box>
|
||||||
|
<div>
|
||||||
|
<Typography gutterBottom sx={{fontWeight: 'medium'}}>
|
||||||
|
{item.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{color: 'grey.400'}}>
|
||||||
|
{item.description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
assets/components/SitemarkIcon.tsx
Normal file
17
assets/components/SitemarkIcon.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import SvgIcon from '@mui/material/SvgIcon';
|
||||||
|
|
||||||
|
export default function SitemarkIcon() {
|
||||||
|
return (
|
||||||
|
<SvgIcon sx={{height: 21, width: 100, mr: 2}}>
|
||||||
|
<svg
|
||||||
|
width={86}
|
||||||
|
height={19}
|
||||||
|
viewBox="0 0 86 19"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
</svg>
|
||||||
|
</SvgIcon>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
assets/components/ToggleColorMode.tsx
Normal file
33
assets/components/ToggleColorMode.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
assets/index.tsx
Normal file
18
assets/index.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
|
||||||
|
import LandingPage from "./pages/LandingPage";
|
||||||
|
import {Route, Routes, HashRouter} from "react-router-dom";
|
||||||
|
import FAQ from "./components/FAQ";
|
||||||
|
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<HashRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<LandingPage/>}/>
|
||||||
|
</Routes>
|
||||||
|
</HashRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
41
assets/pages/LandingPage.tsx
Normal file
41
assets/pages/LandingPage.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {PaletteMode} from '@mui/material';
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import {createTheme, ThemeProvider} from '@mui/material/styles';
|
||||||
|
import AppAppBar from '../components/AppAppBar';
|
||||||
|
import Hero from '../components/Hero';
|
||||||
|
import Highlights from '../components/Highlights';
|
||||||
|
import FAQ from '../components/FAQ';
|
||||||
|
import Footer from '../components/Footer';
|
||||||
|
|
||||||
|
interface ToggleCustomThemeProps {
|
||||||
|
showCustomTheme: Boolean;
|
||||||
|
toggleCustomTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const [mode, setMode] = React.useState<PaletteMode>('light');
|
||||||
|
const defaultTheme = createTheme({palette: {mode}});
|
||||||
|
|
||||||
|
const toggleColorMode = () => {
|
||||||
|
setMode((prev) => (prev === 'dark' ? 'light' : 'dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={defaultTheme}>
|
||||||
|
<CssBaseline/>
|
||||||
|
<AppAppBar mode={mode} toggleColorMode={toggleColorMode}/>
|
||||||
|
<Hero/>
|
||||||
|
<Box sx={{bgcolor: 'background.default'}}>
|
||||||
|
<Divider/>
|
||||||
|
<Highlights/>
|
||||||
|
<Divider/>
|
||||||
|
<FAQ/>
|
||||||
|
<Divider/>
|
||||||
|
<Footer/>
|
||||||
|
</Box>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -16,6 +16,6 @@ return [
|
|||||||
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
|
||||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||||
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
|
||||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||||
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './src/index.tsx',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.tsx', '.ts', '.js'],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: 'bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
41
package.json
41
package.json
@ -1,20 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "domain-watchdog-front",
|
"name": "domain-watchdog",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Maël Gangloff",
|
"name": "Maël Gangloff",
|
||||||
"email": "contact@maelgangloff.fr"
|
"email": "contact@maelgangloff.fr"
|
||||||
},
|
},
|
||||||
"readme": "https://github.com/maelgangloff/domain-watchdog",
|
|
||||||
"homepage": "https://github.com/maelgangloff/domain-watchdog",
|
"homepage": "https://github.com/maelgangloff/domain-watchdog",
|
||||||
|
"readme": "https://github.com/maelgangloff/domain-watchdog",
|
||||||
|
"keywords": [
|
||||||
|
"Domain",
|
||||||
|
"RDAP",
|
||||||
|
"WHOIS"
|
||||||
|
],
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/maelgangloff/domain-watchdog/issues"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.17.0",
|
"@babel/core": "^7.17.0",
|
||||||
"@babel/preset-env": "^7.16.0",
|
"@babel/preset-env": "^7.16.0",
|
||||||
"@babel/preset-react": "^7.24.7",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@symfony/webpack-encore": "^4.6.1",
|
"@emotion/react": "^11.13.0",
|
||||||
"@types/react": "latest",
|
"@emotion/styled": "^11.13.0",
|
||||||
"@types/react-dom": "latest",
|
"@mui/icons-material": "^5.16.4",
|
||||||
|
"@mui/material": "^5.16.4",
|
||||||
|
"@symfony/webpack-encore": "^4.0.0",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
"core-js": "^3.23.0",
|
"core-js": "^3.23.0",
|
||||||
"react-scripts": "^5.0.1",
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.25.1",
|
||||||
"regenerator-runtime": "^0.13.9",
|
"regenerator-runtime": "^0.13.9",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
@ -29,20 +43,5 @@
|
|||||||
"dev": "encore dev",
|
"dev": "encore dev",
|
||||||
"watch": "encore dev --watch",
|
"watch": "encore dev --watch",
|
||||||
"build": "encore production --progress"
|
"build": "encore production --progress"
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"domain",
|
|
||||||
"RDAP",
|
|
||||||
"WHOIS",
|
|
||||||
"tracking",
|
|
||||||
"watchdog",
|
|
||||||
"API"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.12.0",
|
|
||||||
"@emotion/styled": "^11.12.0",
|
|
||||||
"@mui/material": "^5.16.4",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|||||||
class HomeController extends AbstractController
|
class HomeController extends AbstractController
|
||||||
{
|
{
|
||||||
|
|
||||||
|
#[Route(path: "/", name: "index")]
|
||||||
|
public function index(): Response
|
||||||
|
{
|
||||||
|
return $this->render('base.html.twig');
|
||||||
|
}
|
||||||
|
|
||||||
#[Route(path: "/login/oauth", name: "oauth_connect")]
|
#[Route(path: "/login/oauth", name: "oauth_connect")]
|
||||||
public function connectAction(ClientRegistry $clientRegistry): Response
|
public function connectAction(ClientRegistry $clientRegistry): Response
|
||||||
{
|
{
|
||||||
|
|||||||
@ -67,7 +67,7 @@ class OAuthAuthenticator extends OAuth2Authenticator implements AuthenticationEn
|
|||||||
|
|
||||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): RedirectResponse
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): RedirectResponse
|
||||||
{
|
{
|
||||||
return new RedirectResponse($this->router->generate('api_doc'));
|
return new RedirectResponse($this->router->generate('index'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
|||||||
15
templates/base.html.twig
Normal file
15
templates/base.html.twig
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport"
|
||||||
|
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Domain Watchdog</title>
|
||||||
|
{{ encore_entry_link_tags('app') }}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
{{ encore_entry_script_tags('app') }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES6",
|
||||||
|
"jsx": "react",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
73
webpack.config.js
Normal file
73
webpack.config.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const Encore = require('@symfony/webpack-encore');
|
||||||
|
|
||||||
|
// Manually configure the runtime environment if not already configured yet by the "encore" command.
|
||||||
|
// It's useful when you use tools that rely on webpack.config.js file.
|
||||||
|
if (!Encore.isRuntimeEnvironmentConfigured()) {
|
||||||
|
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
|
||||||
|
}
|
||||||
|
|
||||||
|
Encore
|
||||||
|
// directory where compiled assets will be stored
|
||||||
|
.setOutputPath('public/build/')
|
||||||
|
// public path used by the web server to access the output path
|
||||||
|
.setPublicPath('/build')
|
||||||
|
// only needed for CDN's or subdirectory deploy
|
||||||
|
//.setManifestKeyPrefix('build/')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* ENTRY CONFIG
|
||||||
|
*
|
||||||
|
* Each entry will result in one JavaScript file (e.g. app.js)
|
||||||
|
* and one CSS file (e.g. app.css) if your JavaScript imports CSS.
|
||||||
|
*/
|
||||||
|
.addEntry('app', './assets/index.tsx')
|
||||||
|
|
||||||
|
// When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
|
||||||
|
.splitEntryChunks()
|
||||||
|
|
||||||
|
// will require an extra script tag for runtime.js
|
||||||
|
// but, you probably want this, unless you're building a single-page app
|
||||||
|
.enableSingleRuntimeChunk()
|
||||||
|
|
||||||
|
/*
|
||||||
|
* FEATURE CONFIG
|
||||||
|
*
|
||||||
|
* Enable & configure other features below. For a full
|
||||||
|
* list of features, see:
|
||||||
|
* https://symfony.com/doc/current/frontend.html#adding-more-features
|
||||||
|
*/
|
||||||
|
.cleanupOutputBeforeBuild()
|
||||||
|
.enableBuildNotifications()
|
||||||
|
.enableSourceMaps(!Encore.isProduction())
|
||||||
|
// enables hashed filenames (e.g. app.abc123.css)
|
||||||
|
.enableVersioning(Encore.isProduction())
|
||||||
|
|
||||||
|
// configure Babel
|
||||||
|
// .configureBabel((config) => {
|
||||||
|
// config.plugins.push('@babel/a-babel-plugin');
|
||||||
|
// })
|
||||||
|
|
||||||
|
// enables and configure @babel/preset-env polyfills
|
||||||
|
.configureBabelPresetEnv((config) => {
|
||||||
|
config.useBuiltIns = 'usage';
|
||||||
|
config.corejs = '3.23';
|
||||||
|
})
|
||||||
|
|
||||||
|
// enables Sass/SCSS support
|
||||||
|
//.enableSassLoader()
|
||||||
|
|
||||||
|
// uncomment if you use TypeScript
|
||||||
|
.enableTypeScriptLoader()
|
||||||
|
|
||||||
|
// uncomment if you use React
|
||||||
|
.enableReactPreset()
|
||||||
|
|
||||||
|
// uncomment to get integrity="..." attributes on your script & link tags
|
||||||
|
// requires WebpackEncoreBundle 1.4 or higher
|
||||||
|
//.enableIntegrityHashes(Encore.isProduction())
|
||||||
|
|
||||||
|
// uncomment if you're having problems with a jQuery plugin
|
||||||
|
//.autoProvidejQuery()
|
||||||
|
;
|
||||||
|
|
||||||
|
module.exports = Encore.getWebpackConfig();
|
||||||
Loading…
x
Reference in New Issue
Block a user