feat: update front

This commit is contained in:
Maël Gangloff
2024-07-23 18:37:59 +02:00
parent 4556dfe85e
commit 827ca478c6
14 changed files with 599 additions and 126 deletions

View File

@@ -1,47 +1,23 @@
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";
import ToggleColorMode from "./ToggleColorMode";
import {PaletteMode} from "@mui/material";
import Link from "@mui/material/Link";
interface AppAppBarProps {
mode: PaletteMode;
toggleColorMode: () => void;
isAuthenticated: boolean
}
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);
}
};
export default function AppAppBar({mode, toggleColorMode, isAuthenticated}: AppAppBarProps) {
return (
<AppBar
position="fixed"
@@ -73,23 +49,26 @@ export default function AppAppBar({mode, toggleColorMode}: AppAppBarProps) {
<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>
<NavLink to='/'>
<Button
variant="text"
color="info"
size="small"
>
Presentation
</Button>
</NavLink>
</Box>
<Box sx={{display: {xs: 'none', md: 'flex'}}}>
<NavLink to="/dashboard">
<Button
variant="text"
color="info"
size="small"
>
Dashboard
</Button>
</NavLink>
</Box>
</Box>
<Box
@@ -104,42 +83,19 @@ export default function AppAppBar({mode, toggleColorMode}: AppAppBarProps) {
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>
{
!isAuthenticated ?
<NavLink to="/login">
<Button color="primary" variant="text" size="small">
Sign in
</Button>
</MenuItem>
</Box>
</Drawer>
</NavLink>
: <Link href="/logout">
<Button color="primary" variant="text" size="small">
Log out
</Button>
</Link>
}
</Box>
</Toolbar>
</Container>

View File

@@ -5,8 +5,7 @@ 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';
import {NavLink} from "react-router-dom";
function Copyright() {
return (
@@ -41,15 +40,19 @@ export default function Footer() {
}}
>
<div>
<Link color="text.secondary" variant="body2" href="#">
Privacy Policy
</Link>
<NavLink to="/privacy">
<Link color="text.secondary" variant="body2">
Privacy Policy
</Link>
</NavLink>
<Typography sx={{display: 'inline', mx: 0.5, opacity: 0.5}}>
&nbsp;&nbsp;
</Typography>
<Link color="text.secondary" variant="body2" href="#">
Terms of Service
</Link>
<NavLink to="/tos">
<Link color="text.secondary" variant="body2">
Terms of Service
</Link>
</NavLink>
<Copyright/>
</div>
<Stack
@@ -64,7 +67,6 @@ export default function Footer() {
aria-label="GitHub"
sx={{alignSelf: 'center'}}
>
<FacebookIcon/>
</IconButton>
</Stack>
</Box>

1
assets/content/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.md

4
assets/declaration.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module "*.md" {
const content: string;
export default content;
}

View File

@@ -1,18 +1,54 @@
import React from "react";
import React, {useEffect, useState} 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";
import TextPage from "./pages/TextPage";
import {HashRouter, Route, Routes} 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 DashboardPage from "./pages/DashboardPage";
import {getUser} from "./utils/api";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<React.StrictMode>
<HashRouter>
<Routes>
<Route path="/" element={<LandingPage/>}/>
</Routes>
</HashRouter>
function App() {
const [mode, setMode] = React.useState<PaletteMode>('dark')
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
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>
<CssBaseline/>
<AppAppBar mode={mode} toggleColorMode={toggleColorMode} isAuthenticated={isAuthenticated}/>
<Routes>
<Route path="/" element={<LandingPage/>}/>
<Route path="/tos" element={<TextPage content={tosContent}/>}/>
<Route path="/privacy" element={<TextPage content={privacyContent}/>}/>
{isAuthenticated ?
<Route path="/dashboard" element={<DashboardPage/>}/>
:
<Route path="*" element={<LoginPage isAuthenticated={isAuthenticated}
setIsAuthenticated={setIsAuthenticated}/>}/>
}
</Routes>
</HashRouter>
</ThemeProvider>
</React.StrictMode>
);
}
root.render(<App/>)

View File

@@ -0,0 +1,12 @@
import React from 'react';
const DashboardPage = () => {
return (
<>
<p>Dashboard</p>
</>
);
};
export default DashboardPage;

View File

@@ -1,32 +1,14 @@
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/>
@@ -36,6 +18,6 @@ export default function Index() {
<Divider/>
<Footer/>
</Box>
</ThemeProvider>
</>
);
}

102
assets/pages/LoginPage.tsx Normal file
View File

@@ -0,0 +1,102 @@
import * as React 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 Container from '@mui/material/Container';
import Footer from "../components/Footer";
import Link from "@mui/material/Link";
import {login} from "../utils/api";
import {useNavigate} from "react-router-dom";
interface Props {
setIsAuthenticated: (val: boolean) => void
isAuthenticated: boolean
}
export default function LoginPage({setIsAuthenticated, isAuthenticated}: Props) {
const navigate = useNavigate();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
try {
await login(data.get('email') as string, data.get('password') as string);
setIsAuthenticated(true)
navigate('/dashboard');
} catch (e) {
//TODO: handle error
setIsAuthenticated(false)
}
};
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}}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<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>
</>
);
}

38
assets/pages/TextPage.tsx Normal file
View File

@@ -0,0 +1,38 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import Container from "@mui/material/Container";
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>
</Container>
</>
)
}

31
assets/utils/api.ts Normal file
View File

@@ -0,0 +1,31 @@
import axios, {AxiosRequestConfig, AxiosResponse} from "axios";
export async function login(email: string, password: string): Promise<boolean> {
const response = await request({
method: 'POST',
url: 'login',
data: {email, password}
})
return response.status === 200
}
export async function getUser(): Promise<object> {
const response = await request({
url: 'me'
})
return response.data
}
async function request<T = any, R = AxiosResponse<T>, D = any>(config: AxiosRequestConfig): Promise<R> {
const axiosConfig: AxiosRequestConfig = {
...config,
baseURL: '/api',
withCredentials: true,
headers: {
Accept: 'application/ld+json'
}
}
return await axios.request<T, any, D>(axiosConfig)
}