feat: add frontend

This commit is contained in:
Maël Gangloff
2024-07-22 14:45:21 +02:00
parent 1642767993
commit 3d7a6fbcfd
21 changed files with 5818 additions and 44 deletions

View 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
View 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>
);
}

View 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&nbsp;</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}}>
&nbsp;&nbsp;
</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>
);
}

View 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&nbsp;
<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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

18
assets/index.tsx Normal file
View 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>
);

View 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>
);
}