Skip to content
Snippets Groups Projects
Commit d9ebfd2a authored by Bernard Roland (Student Com20)'s avatar Bernard Roland (Student Com20)
Browse files

Merge branch 'frontend-devel' into devel

parents 8669c676 41679deb
No related branches found
No related tags found
No related merge requests found
Showing
with 465 additions and 85 deletions
......@@ -14,7 +14,7 @@
<meta name="name" content="ryoko | plan your projects like a journey">
<meta property="og:title" content="Try planning your next project with ryoko!">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
<title>ryoko | plan your projects like a journey</title>
......
import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import ProtectedRoute from 'components/helpers/Rerouters/ProtectedRoute';
import LoginRoute from 'components/helpers/Rerouters/LoginRoute';
import { BrowserRouter as Router, Switch, Route, } from 'react-router-dom';
import LoginRoute from 'components/helpers/LoginRoute';
import AppWrapper from 'pages/AppWrapper';
const Home = lazy(() => import('pages/Home'));
const Login = lazy(() => import('pages/Login'));
const Register = lazy(() => import('pages/Register'));
const Tasks = lazy(() => import('pages/Tasks'));
const Projects = lazy(() => import('pages/Projects'));
const Stats = lazy(() => import('pages/Stats'));
export default function App() {
return (
<Router>
<Suspense fallback={false}>
<Switch>
<ProtectedRoute path="/tasks" component={<Tasks />} />
<ProtectedRoute path="/projects" component={<Projects />} />
<ProtectedRoute path="/stats" component={<Stats />} />
<LoginRoute path="/login" component={Login} />
<LoginRoute path="/register" component={Register} />
<Route path={['/tasks', '/projects', '/stats', '/teams', '/settings']} component={AppWrapper} />
<Route path="/" component={Home} />
</Switch>
</Suspense>
......
export const isLoggedIn = (): boolean => true;
import { apiRoot } from 'config';
export function getAuthHeader(): HeadersInit {
if (isLoggedIn()) {
return {
'Authorization': `Brearer ${getToken()}`,
};
} else {
return {};
}
}
export function isLoggedIn(): boolean {
return typeof getToken() === 'string';
}
export function getToken(): string | null {
return localStorage.getItem('access-token');
}
export function setToken(token: string) {
localStorage.setItem('access-token', token);
}
export function clearToken() {
localStorage.removeItem('access-token');
}
extendAccessToken();
setInterval(extendAccessToken, 1000 * 60 * 30);
async function extendAccessToken() {
if (isLoggedIn()) {
try {
const response = await fetch(`${apiRoot}/auth/extend`, { headers: getAuthHeader() });
if (response.ok) {
const json = await response.json();
setToken(json.token);
} else if (response.status === 403) {
clearToken();
}
} catch(e) {
clearToken();
}
}
}
export async function register(username: string, password: string): Promise<boolean> {
try {
const response = await fetch(`${apiRoot}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password,
}),
});
if (response.ok) {
const json = await response.json();
setToken(json.token);
}
return response.ok;
} catch (e) {
// Probably a network error
throw e;
}
}
export async function login(username: string, password: string): Promise<boolean> {
try {
const response = await fetch(`${apiRoot}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: username,
password: password
}),
});
if (response.ok) {
const json = await response.json();
setToken(json.token);
}
return response.ok;
} catch (e) {
// Probably a network error
throw e;
}
}
export const DEMO_CONSTANT = 42;
import { apiRoot } from 'config';
export async function exists(username: string) {
try {
const response = await fetch(`${apiRoot}/user/name/${username}`);
return response.ok;
} catch (e) {
// Probably a network error
return false;
}
}
@use 'styles/mixins'as mx;
.contact-form {
.input-element {
width: 100%;
@include mx.breakpoint(medium) {
width: 50%;
}
&.textarea {
width: 100%;
}
}
.submit-button {
appearance: none;
border: none;
margin: 0.5rem;
}
.button-container {
display: flex;
width: 100%;
justify-content: flex-end;
}
}
import { FormEvent, useCallback, useState } from "react";
import TextInput from 'components/ui/TextInput';
import Button from 'components/ui/Button';
import './contact-form.scss';
interface Props {
onSubmit?: (firstname: string, lastname: string, email: string, subject: string, message: string) => void
}
export default function RegisterForm({ onSubmit }: Props) {
const [firstname, setFirstname] = useState<string>('');
const [lastname, setLastname] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [subject, setSubject] = useState<string>('');
const [message, setMessage] = useState<string>('');
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
onSubmit?.(firstname, lastname, email, subject, message);
}, [onSubmit, firstname, lastname, email, subject, message]);
return (
<form className="contact-form" onSubmit={handleSubmit}>
<TextInput
label="Firstname"
name="firstname"
onChange={setFirstname}
/>
<TextInput
label="Lastname"
name="lastname"
onChange={setLastname}
/>
<TextInput
label="Email"
name="email"
onChange={setEmail}
/>
<TextInput
label="Subject"
name="subject"
onChange={setSubject}
/>
<TextInput
label="Message"
name="message"
type="textarea"
onChange={setMessage}
/>
<div className="button-container">
<Button type="submit" className="submit-button">
Login
</Button>
</div>
</form>
);
}
import { FormEvent, useCallback, useState } from "react";
import TextInput from 'components/ui/TextInput';
import Button from 'components/ui/Button';
import './login-form.scss';
interface Props {
onSubmit?: (username: string, password: string) => void
}
export default function RegisterForm({ onSubmit }: Props) {
const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>('');
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
onSubmit?.(username, password);
}, [onSubmit, password, username]);
return (
<form className="login-form" onSubmit={handleSubmit}>
<TextInput
label="Username"
name="username"
color="dark"
onChange={setUsername}
/>
<TextInput
label="Password"
name="password"
color="dark"
type="password"
onChange={setPassword}
/>
<Button type="submit">
Login
</Button>
</form>
);
}
.login-form {
.button {
width: 100%;
max-width: 300px;
display: block;
margin: 0 auto;
margin-top: 40px;
}
}
import { FormEvent, useCallback, useState } from 'react';
import TextInput from 'components/ui/TextInput';
import Button from 'components/ui/Button';
import { exists } from 'adapters/user';
import './register-form.scss';
async function validateUsername(username: string) {
if (username?.length < 3) {
return 'Username has to be at least 4 characters long.';
} else if (await exists(username)) {
return 'Username is already taken by someone else.';
} else {
return null;
}
}
function validatePassword(password: string) {
if (password?.length < 6) {
return 'Password has to be at least 6 characters long';
} else {
return null;
}
}
function validateRepeatPassword(password: string, password2: string) {
if (password !== password2) {
return 'The passwords are not the same.';
} else {
return null;
}
}
interface Props {
onSubmit?: (username: string, password: string) => void
}
export default function RegisterForm({ onSubmit }: Props) {
const [username, setUsername] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [repeatedPassword, setRepeatedPassword] = useState<string>('');
const handleSubmit = useCallback(async (e: FormEvent) => {
e.preventDefault();
if (await validateUsername(username) === null && validatePassword(password) === null && validateRepeatPassword(repeatedPassword, password) === null) {
onSubmit?.(username, password);
}
}, [onSubmit, password, username, repeatedPassword]);
return (
<form className="register-form" onSubmit={handleSubmit}>
<TextInput
label="Username"
name="username"
color="dark"
onChange={setUsername}
validation={validateUsername}
/>
<TextInput
label="Password"
name="password"
color="dark"
type="password"
onChange={setPassword}
validation={validatePassword}
/>
<TextInput
label="Repeat password"
name="repeat-password"
color="dark"
type="password"
onChange={setRepeatedPassword}
compareValue={password}
validation={validateRepeatPassword}
/>
<Button type="submit">
Register now
</Button>
</form>
);
}
.register-form {
.button {
width: 100%;
max-width: 300px;
display: block;
margin: 0 auto;
margin-top: 40px;
}
}
export default function LineGraph() {
return (
<div className="line-graph">
<svg viewBox="0 0 260 90" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.2015 75.7723C20.1112 70.0477 37.6267 81.5131 63.9321 78.3076C90.2375 75.102 102.251 20.7496 131.038 17.2416C159.825 13.7336 179.555 30.9736 198.912 28.6147C218.269 26.2559 235.267 4.54031 256.113 2.00004" stroke="url(#paint0_linear)" strokeWidth="4" />
<defs>
<linearGradient id="paint0_linear" x1="262" y1="2" x2="21" y2="85" gradientUnits="userSpaceOnUse">
<stop stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0.61" />
</linearGradient>
</defs>
</svg>
</div>
)
}
\ No newline at end of file
import { Route, RouteProps, useHistory } from 'react-router-dom';
import { isLoggedIn } from 'adapters/auth';
import { useEffect } from 'react';
export default function LoginRoute(props: RouteProps) {
const history = useHistory();
useEffect(() => {
if (isLoggedIn()) {
if (history.length === 0) {
history.push('/tasks');
} else {
history.goBack();
}
}
})
return (
<Route {...props} />
);
}
import { Route, RouteProps, useHistory } from 'react-router-dom';
import { isLoggedIn } from 'adapters/auth';
import { useEffect } from 'react';
export default function ProtectedRoute(props: RouteProps) {
const history = useHistory();
useEffect(() => {
if (!isLoggedIn()) {
history.push('/login');
}
})
return (
<Route {...props} />
);
}
import { ComponentType } from 'react';
import { Redirect, Route } from 'react-router-dom';
import { isLoggedIn } from 'adapters/api';
interface Props {
path: string,
exact?: boolean,
component: ComponentType<any>
}
export default function ProtectedRoute({ path, exact, component }: Props) {
if (!isLoggedIn()) {
return (
<Route path={path} exact={exact} component={component} />
);
}
return (
<Redirect to="/tasks"/>
);
}
\ No newline at end of file
import { Redirect, Route } from 'react-router-dom';
import { isLoggedIn } from 'adapters/api';
import Navigation from 'components/ui/Navigation';
import Header from 'components/ui/Header';
interface Props {
path: string,
exact?: boolean,
component: JSX.Element
}
export default function ProtectedRoute({ path, exact, component }: Props) {
console.log(component);
if (isLoggedIn()) {
return (
<Route path={path} exact={exact} render={() =>
<>
<Header />
<Navigation />
{ component }
</>
} />
);
}
return (
<Redirect to="/login" />
);
}
\ No newline at end of file
@use 'styles/settings';
@use 'styles/functions';
@use 'styles/settings' as s;
@use 'styles/functions' as fn;
.button {
padding: 14px 50px;
font-size: functions.toRem(18);
font-weight: settings.$weight-bold;
background: radial-gradient(60% 100% at 50% 0%, #D298FF 0%, settings.$primary 100%);
box-shadow: 0px 5px 15px rgba(settings.$primary, 0.25);
font-size: fn.toRem(18);
font-weight: s.$weight-bold;
background: s.$linear-gradient;
box-shadow: 0px 5px 15px rgba(s.$primary, 0.25);
border-radius: 25px;
display: inline-block;
color: settings.$white;
color: s.$white;
text-transform: uppercase;
user-select: none;
appearance: none;
border: none;
outline: none;
&:hover,
&:focus {
box-shadow: 0px 10px 25px rgba(settings.$primary, 0.35);
box-shadow: 0px 10px 25px rgba(s.$primary, 0.35);
cursor: pointer;
color: settings.$white;
color: s.$white;
transform: translateY(-5%);
}
&:active {
transform: scale(0.9);
}
}
}
\ No newline at end of file
@use 'styles/settings'as s;
@use 'styles/mixins'as mx;
.full-width {
width: 100%;
height: 100%;
overflow: hidden;
}
.page-wrapper {
&.moved {
transform: translateX(340px);
}
@include mx.breakpoint(large) {
padding-left: 340px;
.page-container {
max-width: 1280px;
@include mx.breakpoint(large) {
padding: 4rem 60px;
&.moved {
transform: translateX(0);
}
}
}
}
}
.site-header {
display: flex;
justify-content: space-between;
background: rgba(s.$white, 0.5);
backdrop-filter: blur(10px);
@include mx.breakpoint(large) {
display: none;
}
img {
padding: 35px;
padding: 35px 30px 20px 30px;
}
}
\ No newline at end of file
import './header.scss';
import hamburger from 'images/svg/hamburger.svg';
import profile from 'images/svg/profile.svg';
import Navigation from 'components/ui/Navigation';
import Sidebar from 'components/ui/Sidebar';
import { ReactNode, useState } from 'react';
export default function Header() {
return (
<header className="site-header">
<img src={hamburger} alt="Navigation"/>
<img src={profile} alt="Profile"/>
</header>
);
interface Props {
children?: ReactNode
}
export default function Header({ children }: Props) {
const [showSidebar, setShowSidebar] = useState<boolean>(false);
return (
<div className="full-width">
<Sidebar mobileShown={showSidebar} />
<div className={'page-wrapper' + (showSidebar ? ' moved' : '')} onClick={() => showSidebar && setShowSidebar(false)}>
<header className="site-header">
<img src={hamburger} alt="Navigation" onClick={() => !showSidebar && setShowSidebar(true)} />
</header>
<Navigation />
{children}
</div>
</div>
);
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment