diff --git a/client/public/index.html b/client/public/index.html index f3cdd31fc9d9f6e576c0da7fb067f4a66310d79f..c67ac5bb2f86d87c6a2281191eb747c188e80d5e 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -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> diff --git a/client/src/App.tsx b/client/src/App.tsx index 38ea359fb60fce8f888b6dae8dfcccc2ed6f4e68..54df0d373bc6462e828d075686b1ab7dadce0ad1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,26 +1,22 @@ 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> diff --git a/client/src/adapters/api.ts b/client/src/adapters/api.ts deleted file mode 100644 index 28227040739216551352419a16fb0f27020e02e7..0000000000000000000000000000000000000000 --- a/client/src/adapters/api.ts +++ /dev/null @@ -1 +0,0 @@ -export const isLoggedIn = (): boolean => true; diff --git a/client/src/adapters/auth.ts b/client/src/adapters/auth.ts new file mode 100644 index 0000000000000000000000000000000000000000..a8d1ad5e0f5ee36b4c9274fc5b4bdf4b256afb8b --- /dev/null +++ b/client/src/adapters/auth.ts @@ -0,0 +1,93 @@ + +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; + } +} diff --git a/client/src/adapters/constants.ts b/client/src/adapters/constants.ts deleted file mode 100644 index c5c18b169fcc2ade194ac898739505c92a1fdff9..0000000000000000000000000000000000000000 --- a/client/src/adapters/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ - -export const DEMO_CONSTANT = 42; - diff --git a/client/src/adapters/user.ts b/client/src/adapters/user.ts new file mode 100644 index 0000000000000000000000000000000000000000..655b475eaed9c3493c7d661b9bcfe417abda9b1d --- /dev/null +++ b/client/src/adapters/user.ts @@ -0,0 +1,13 @@ + +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; + } +} + diff --git a/client/src/components/forms/ContactForm/contact-form.scss b/client/src/components/forms/ContactForm/contact-form.scss new file mode 100644 index 0000000000000000000000000000000000000000..65427fc2382cfc17b9fab36f499bd57ce61b68e7 --- /dev/null +++ b/client/src/components/forms/ContactForm/contact-form.scss @@ -0,0 +1,27 @@ +@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; + } +} diff --git a/client/src/components/forms/ContactForm/index.tsx b/client/src/components/forms/ContactForm/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bdcc4ffe14e6eb69ac8979578c4dc96f756984df --- /dev/null +++ b/client/src/components/forms/ContactForm/index.tsx @@ -0,0 +1,58 @@ +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> + ); +} + diff --git a/client/src/components/forms/LoginForm/index.tsx b/client/src/components/forms/LoginForm/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..aebf7c8e0fbebb9103a52ba472bfc5c959c65681 --- /dev/null +++ b/client/src/components/forms/LoginForm/index.tsx @@ -0,0 +1,40 @@ +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> + ); +} + diff --git a/client/src/components/forms/LoginForm/login-form.scss b/client/src/components/forms/LoginForm/login-form.scss new file mode 100644 index 0000000000000000000000000000000000000000..591a64e849c8040d1a01ea1ae5bb8984a8d5bbc2 --- /dev/null +++ b/client/src/components/forms/LoginForm/login-form.scss @@ -0,0 +1,9 @@ +.login-form { + .button { + width: 100%; + max-width: 300px; + display: block; + margin: 0 auto; + margin-top: 40px; + } +} diff --git a/client/src/components/forms/RegisterForm/index.tsx b/client/src/components/forms/RegisterForm/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..7dcd61c534e05d7eaf7d37405ee463b59c825a81 --- /dev/null +++ b/client/src/components/forms/RegisterForm/index.tsx @@ -0,0 +1,83 @@ + +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> + ); +} + diff --git a/client/src/components/forms/RegisterForm/register-form.scss b/client/src/components/forms/RegisterForm/register-form.scss new file mode 100644 index 0000000000000000000000000000000000000000..16eed791820ec060ad8e6f385ce5f9ebb46214a5 --- /dev/null +++ b/client/src/components/forms/RegisterForm/register-form.scss @@ -0,0 +1,10 @@ + +.register-form { + .button { + width: 100%; + max-width: 300px; + display: block; + margin: 0 auto; + margin-top: 40px; + } +} diff --git a/client/src/components/graphs/LineGraph/index.tsx b/client/src/components/graphs/LineGraph/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d3c34d6e0aa0ce39d199cd8ef5b08f44a26b4661 --- /dev/null +++ b/client/src/components/graphs/LineGraph/index.tsx @@ -0,0 +1,15 @@ +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 diff --git a/client/src/components/helpers/LoginRoute.tsx b/client/src/components/helpers/LoginRoute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..47f8be758d41d889732782c6573180609ea96128 --- /dev/null +++ b/client/src/components/helpers/LoginRoute.tsx @@ -0,0 +1,21 @@ + +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} /> + ); +} + diff --git a/client/src/components/helpers/ProtectedRoute.tsx b/client/src/components/helpers/ProtectedRoute.tsx new file mode 100644 index 0000000000000000000000000000000000000000..136b7e6b1e9c5730bd34a545450aa10480042845 --- /dev/null +++ b/client/src/components/helpers/ProtectedRoute.tsx @@ -0,0 +1,17 @@ + +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} /> + ); +} + diff --git a/client/src/components/helpers/Rerouters/LoginRoute.tsx b/client/src/components/helpers/Rerouters/LoginRoute.tsx deleted file mode 100644 index 56de5692595ad31d6bddc743d29bb81fa2c073e0..0000000000000000000000000000000000000000 --- a/client/src/components/helpers/Rerouters/LoginRoute.tsx +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/client/src/components/helpers/Rerouters/ProtectedRoute.tsx b/client/src/components/helpers/Rerouters/ProtectedRoute.tsx deleted file mode 100644 index c3f3daeb8e0d5f5cabaab8ac164a46f8526fed88..0000000000000000000000000000000000000000 --- a/client/src/components/helpers/Rerouters/ProtectedRoute.tsx +++ /dev/null @@ -1,32 +0,0 @@ - -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 diff --git a/client/src/components/ui/Button/button.scss b/client/src/components/ui/Button/button.scss index 9a1a2fbc28b92d8dffa064dfe420814b4db75c39..89a28ab8cb99c33f2a374a6ebaca3358866cf06d 100644 --- a/client/src/components/ui/Button/button.scss +++ b/client/src/components/ui/Button/button.scss @@ -1,31 +1,30 @@ - -@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 diff --git a/client/src/components/ui/Header/header.scss b/client/src/components/ui/Header/header.scss index 4d137387d1c6b19dd9d52d0998bbef6d5e25bc06..830b9e599553166a16edd88fd1af57ff857178f4 100644 --- a/client/src/components/ui/Header/header.scss +++ b/client/src/components/ui/Header/header.scss @@ -1,7 +1,47 @@ +@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 diff --git a/client/src/components/ui/Header/index.tsx b/client/src/components/ui/Header/index.tsx index 22257f96381524142f394bbab0ee8e299d48e709..f0360d321ac3c28a2951bfb098a12da8457cf3b6 100644 --- a/client/src/components/ui/Header/index.tsx +++ b/client/src/components/ui/Header/index.tsx @@ -1,12 +1,26 @@ 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 diff --git a/client/src/components/ui/Navigation/index.tsx b/client/src/components/ui/Navigation/index.tsx index 4a019f25a2382ee193855389775eea4aa0001a05..7d1ecc399235a117fee4f16dd0f7efbb6ed4cd37 100644 --- a/client/src/components/ui/Navigation/index.tsx +++ b/client/src/components/ui/Navigation/index.tsx @@ -1,7 +1,6 @@ import { NavLink } from 'react-router-dom'; import './navigation.scss'; -import background from 'images/svg/nav-bg.svg'; export default function Navigation() { @@ -15,17 +14,16 @@ export default function Navigation() { </NavLink> <NavLink to="/projects" activeClassName="active" className="nav-link"> <span className="icon material-icons-outlined"> - public + folder </span> <span className="label">Projects</span> </NavLink> - <NavLink to="/stats" activeClassName="active" className="nav-link"> + <NavLink to="/teams" activeClassName="active" className="nav-link"> <span className="icon material-icons-outlined"> - public + people </span> - <span className="label">Stats</span> + <span className="label">Teams</span> </NavLink> - <img src={background} alt="Background" className="background" /> </nav> ); } \ No newline at end of file diff --git a/client/src/components/ui/Navigation/navigation.scss b/client/src/components/ui/Navigation/navigation.scss index 5d192c40213dbd3ad8f0ab6d3994748567a1ceb9..bc29dac3d3770aeb2277184c7ea39877d2680c6a 100644 --- a/client/src/components/ui/Navigation/navigation.scss +++ b/client/src/components/ui/Navigation/navigation.scss @@ -1,4 +1,5 @@ @use 'styles/settings.scss'as s; +@use 'styles/mixins.scss'as mx; .site-nav { position: fixed; @@ -9,14 +10,12 @@ display: flex; align-items: center; justify-content: center; + z-index: 200; + background: s.$white; + box-shadow: 0 0 25px rgba(s.$black, 0.1); - .background { - position: absolute; - bottom: 0; - left: 0; - max-width: 100%; - height: auto; - z-index: -1; + @include mx.breakpoint(medium) { + display: none; } .nav-link { @@ -34,16 +33,15 @@ position: absolute; opacity: 0; font-size: 12px; - top: 50%; - left: 60px; - transform: translateY(-50%); - z-index: -1; + bottom: 0; + transform: translate(-50%, 10%); + left: 50% } &.active { - padding-right: 60px; .icon { font-family: 'Material Icons'; + transform: translateY(-5px); } .label { @@ -51,4 +49,4 @@ } } } -} \ No newline at end of file +} diff --git a/client/src/components/ui/Page/index.tsx b/client/src/components/ui/Page/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..73ce518081213449bdb9032ea15765d5a2e2a60a --- /dev/null +++ b/client/src/components/ui/Page/index.tsx @@ -0,0 +1,18 @@ + +import { ReactNode } from 'react'; + +import './page.scss'; + +interface Props { + children?: ReactNode, + className?: string, +} + +export default function Page({ children, className }: Props) { + return ( + <main className={'page-container ' + (className ?? '')}> + {children} + </main> + ); +} + diff --git a/client/src/components/ui/Page/page.scss b/client/src/components/ui/Page/page.scss new file mode 100644 index 0000000000000000000000000000000000000000..88bca20bd430711abef786df7da94c96faa30238 --- /dev/null +++ b/client/src/components/ui/Page/page.scss @@ -0,0 +1,17 @@ +@use 'styles/mixins' as mx; +body { + @include mx.breakpoint(xlarge) { + padding-top: 5rem; + } +} +.page-container { + max-width: 1540px; + min-height: 100vh; + margin: 0 auto; + background: rgba(255, 255, 255, 0.5); + backdrop-filter: blur(10px); + @include mx.breakpoint(xlarge) { + min-height: calc(100vh - 5rem); + } +} + diff --git a/client/src/components/ui/Sidebar/index.tsx b/client/src/components/ui/Sidebar/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..af2ad0a56793588b59d5c8a4bb6b02e8006ba068 --- /dev/null +++ b/client/src/components/ui/Sidebar/index.tsx @@ -0,0 +1,59 @@ +import Navigation from 'components/ui/Navigation'; +import avatar from 'images/daniel-planoetscher.jpg'; +import LineGraph from 'components/graphs/LineGraph'; +import { NavLink, useHistory } from 'react-router-dom'; +import { clearToken } from 'adapters/auth'; +import './sidebar.scss'; + +interface Props { + mobileShown: boolean; +} + +export default function Sidebar({ mobileShown }: Props) { + + const history = useHistory(); + + const logout = () => { + clearToken(); + history.push('/login'); + } + + return ( + <aside className={'site-aside' + (mobileShown ? ' shown' : '')}> + <div className="top"> + <div className="profile"> + <div className="avatar"> + <img src={avatar} alt="Profile" /> + </div> + <span className="name">Daniel Planötscher</span> + <span className="team">ryoko</span> + </div> + <Navigation /> + <nav className="secondary-nav"> + <NavLink to="/stats" className="nav-link"> + <span className="icon material-icons-outlined"> + assessment + </span> + Stats + </NavLink> + <NavLink to="/settings" className="nav-link"> + <span className="icon material-icons-outlined"> + settings + </span> + Settings + </NavLink> + <button className="nav-link" onClick={logout}> + <span className="icon material-icons-outlined"> + logout + </span> + Logout + </button> + </nav> + </div> + <div className="stats"> + <LineGraph /> + <div className="comment">You are doing well!</div> + </div> + </aside> + ); +} \ No newline at end of file diff --git a/client/src/components/ui/Sidebar/sidebar.scss b/client/src/components/ui/Sidebar/sidebar.scss new file mode 100644 index 0000000000000000000000000000000000000000..b8691aea971025cc5dcf4de02a938e44d47c064b --- /dev/null +++ b/client/src/components/ui/Sidebar/sidebar.scss @@ -0,0 +1,150 @@ +@use 'styles/settings.scss'as s; +@use 'styles/mixins.scss'as mx; +@use 'styles/functions.scss'as fn; + +.site-aside { + position: fixed; + left: 0; + top: 0; + max-width: 340px; + width: 100%; + height: 100vh; + background: s.$dark; + z-index: 2000; + border-top-right-radius: 25px; + border-bottom-right-radius: 25px; + transform: translateX(-100%); + visibility: hidden; + padding: 50px 30px; + color: s.$white; + overflow: auto; + display: flex; + flex-direction: column; + justify-content: space-between; + + &.shown { + transform: translateX(0); + visibility: visible; + } + + @include mx.breakpoint(large) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + transform: translateX(0); + visibility: visible; + } + + + .profile { + margin-bottom: 50px; + + .avatar { + width: 130px; + height: 130px; + border-radius: 50%; + overflow: hidden; + margin-bottom: 20px; + + img { + width: 100%; + height: auto; + } + } + + .name { + font-size: fn.toRem(20); + font-weight: s.$weight-semi-bold; + display: block; + } + + .team { + font-size: fn.toRem(14); + } + } + + .nav-link { + display: flex; + align-items: center; + color: s.$white; + opacity: 0.7; + + .icon { + padding-right: 10px; + } + + &.active, + &:hover { + opacity: 1; + } + + &.active { + .icon { + font-family: 'Material Icons'; + transform: none; + } + } + } + + .site-nav { + display: none; + position: relative; + background: transparent; + box-shadow: none; + height: auto; + padding-bottom: 30px; + margin-bottom: 30px; + + @include mx.breakpoint(medium) { + display: block; + } + + &:after { + content: ' '; + position: absolute; + bottom: 0; + left: 0; + height: 2px; + width: 40px; + background: rgba(s.$white, 0.1); + + } + + .nav-link { + padding: 10px 0; + + .label { + position: static; + transform: none; + opacity: 1; + font-size: fn.toRem(20); + } + } + + .background { + display: none; + } + } + + .secondary-nav { + display: block; + + .nav-link { + font-size: fn.toRem(18); + padding: 5px 0; + } + } + + .stats { + margin-top: 30px; + display: flex; + align-items: center; + flex-direction: column; + font-size: fn.toRem(14); + font-weight: s.$weight-semi-bold; + + .line-graph { + width: 100%; + margin-bottom: 10px; + } + } +} \ No newline at end of file diff --git a/client/src/components/ui/Task/index.tsx b/client/src/components/ui/Task/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f61ede773ef307cdf3ccaf8b8f6294df02c2fc3e --- /dev/null +++ b/client/src/components/ui/Task/index.tsx @@ -0,0 +1,56 @@ +import './task.scss'; +import { Link } from 'react-router-dom'; +import Tooltip from 'components/ui/Tooltip'; +import avatar from 'images/daniel-planoetscher.jpg'; + +interface TaskInt { + uuid: string, + name: string, + icon: string, + start: number, + end: number, + description: string +} + +interface Props { + task: TaskInt, + active?: boolean, +} + +function formattedTime(date: Date) { + return date.getHours() + ':' + (date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()); +} + +export default function Task({ task, active }: Props) { + const start = new Date(task.start); + const end = new Date(task.end); + + + + return ( + <Link to={'/tasks/' + task.uuid} className="task"> + <div className="tag"></div> + <div className="main-info"> + <div className="icon-container"> + {task.icon} + </div> + <div className="text-container"> + <h4>{task.name}</h4> + <div className="time">{formattedTime(start)} - {formattedTime(end)}</div> + </div> + </div> + <div className="description-container"> + {task.description} + </div> + <div className="assignees-container"> + <Tooltip text="Daniel Planötscher" className="assignee"> + <img src={avatar} alt="Someone"/> + </Tooltip> + <Tooltip text="Daniel Planötscher" className="assignee"> + <img src={avatar} alt="Someone"/> + </Tooltip> + </div> + + </Link> + ) +} \ No newline at end of file diff --git a/client/src/components/ui/Task/task.scss b/client/src/components/ui/Task/task.scss new file mode 100644 index 0000000000000000000000000000000000000000..418019158e77e1993a86a4750434546d0d14d9d7 --- /dev/null +++ b/client/src/components/ui/Task/task.scss @@ -0,0 +1,98 @@ +@use 'styles/settings.scss'as s; + +.task { + padding: 30px; + border-radius: 10px; + background: s.$white; + width: 100%; + box-shadow: 0 0px 20px rgba(s.$black, 0.05); + display: block; + color: s.$body-color; + font-weight: s.$weight-regular; + position: relative; + + &:hover { + color: s.$body-color; + box-shadow: 0 5px 30px rgba(s.$black, 0.15); + transform: translateY(-5px); + + .tag { + height: 40%; + } + } + + .tag { + position: absolute; + left: 0; + top: 50%; + transform: translate(-50%, -50%); + background: s.$primary; + width: 6px; + height: 50%; + border-radius: 3px; + } + + + .main-info { + display: flex; + align-items: center; + + .icon-container { + font-size: 24px; + margin-right: 15px; + width: 50px; + height: 50px; + background: s.$light-gray; + display: flex; + justify-content: center; + align-items: center; + border-radius: 10px; + } + + h4 { + margin-bottom: 0; + } + + .time { + opacity: .75; + font-size: 12px; + } + } + + .description-container { + margin-top: 20px; + display: none; + } + + &:first-child { + .description-container { + display: block; + } + } + + .assignees-container { + position: absolute; + right: -15px; + bottom: -20px; + display: flex; + + + &:hover { + .assignee { + margin-left: 0; + } + } + + .assignee { + + margin-left: -15px; + + img { + width: 30px; + height: 30px; + border-radius: 50%; + } + + } + } +} \ No newline at end of file diff --git a/client/src/components/ui/TextInput/index.tsx b/client/src/components/ui/TextInput/index.tsx index 1613bebc18a01ee49e358bc5e2e7ee76767ac3aa..37f8a88a93a6ea89d7ab8cfbbd145ab237c327a5 100644 --- a/client/src/components/ui/TextInput/index.tsx +++ b/client/src/components/ui/TextInput/index.tsx @@ -1,24 +1,41 @@ -import React from "react"; + +import { ChangeEvent, Dispatch, FocusEvent, useCallback, useState } from "react"; import './text-input.scss'; interface Props { label: string, name: string, - type?: string + color?: 'dark' + type?: 'password' | 'textarea' | 'text', + compareValue?: string, + onChange: Dispatch<string>, + validation?: ((text: string) => Promise<string | null> | string | null) | ((value1: string, value2: string) => Promise<string | null> | string | null); } -export default function TextInput({ label, name, type }: Props) { - type = type ? type : 'text'; +export default function TextInput({ label, name, type, color, onChange, validation, compareValue }: Props) { + const [error, setError] = useState(''); + + const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + onChange(e.target.value); + }, [onChange]); + + const handleBlur = useCallback(async (e: FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => { + let error = await validation?.(e.target.value, compareValue ?? ''); + setError(error ?? ''); + }, [validation, compareValue]); return ( - <div className={'input-field' + (type === 'textarea' ? ' textarea' : '')}> - <label htmlFor={name}>{label}</label> - { - type === 'textarea' ? - (<textarea name={name} id={name} required></textarea>) - : (<input type={type} required name={name} id={name} />) - } + <div className={'input-element' + (type === 'textarea' ? ' textarea' : '')}> + <div className={'input-field ' + (color ?? '')}> + <label htmlFor={name}>{label}</label> + { + type === 'textarea' ? + (<textarea onChange={handleChange} name={name} id={name} onBlur={handleBlur} />) + : (<input onChange={handleChange} type={type} name={name} id={name} onBlur={handleBlur} autoComplete="off" />) + } + </div > + {error && (<div className="error">{error}</div>)} </div> ); } diff --git a/client/src/components/ui/TextInput/text-input.scss b/client/src/components/ui/TextInput/text-input.scss index d067bd80cc110c6896fb3076ab9fdfe257aedf57..d8eb4b1ca0f68ef7e5c1801045bd1bcd62e49c8c 100644 --- a/client/src/components/ui/TextInput/text-input.scss +++ b/client/src/components/ui/TextInput/text-input.scss @@ -1,61 +1,64 @@ -@use 'styles/settings.scss' as s; -@use 'styles/mixins.scss' as mx; -.input-field { - position: relative; +@use 'styles/settings.scss'as s; +@use 'styles/mixins.scss'as mx; + +.input-element { margin: 20px 0 30px 0; - padding: 0 5px; - width: 100%; - @include mx.breakpoint(medium) { - width: 50%; + .error { + color: s.$error-color; + margin: 10px 0 20px 0; + padding-left: 15px; + line-height: 1; } - &.textarea { + .input-field { + position: relative; + padding: 0 5px; width: 100%; - } - &:before { - content: ' '; - position: absolute; - left: 5px; - top: 10px; - width: 100%; - height: 100%; - background: rgba(255, 255, 255, 0.05); - border-radius: 15px; - filter: blur(10px); - } + &.dark { + textarea, + input { + color: s.$body-color; + background: rgba(0, 0, 0, 0.025); + } - label { - font-size: 16px; - position: absolute; - left: 20px; - font-weight: s.$weight-bold; - z-index: 20; - transform: translateY(-50%); + &:before { + display: none; + } + } - } + label { + font-size: 16px; + position: absolute; + left: 20px; + font-weight: s.$weight-bold; + z-index: 20; + transform: translateY(-50%); + } - textarea { - resize: none; - height: 300px; - } + textarea { + resize: none; + height: 300px; + } - input, - textarea { - width: 100%; - font-size: 16px; - border: none; - padding: 20px; - outline: none; - border-radius: 15px; - position: relative; - display: block; - border-radius: 15px; - color: #fff; - font-weight: s.$weight-regular; - font-family: s.$body-font; - background: rgba(255, 255, 255, 0.1); + input, + textarea { + width: 100%; + font-size: 16px; + border: none; + padding: 20px; + outline: none; + border-radius: 15px; + position: relative; + display: block; + border-radius: 15px; + color: #fff; + font-weight: s.$weight-regular; + font-family: s.$body-font; + background: rgba(255, 255, 255, 0.1); + } } } + diff --git a/client/src/components/ui/Tooltip/index.tsx b/client/src/components/ui/Tooltip/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1705ced429d5a2b256bd308e4afa4592d16a04ae --- /dev/null +++ b/client/src/components/ui/Tooltip/index.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; +import './tooltip.scss'; + +interface Props { + text: string, + className?: string, + children: ReactNode +} + +export default function Tooltip({children, text, className}: Props) { + return ( + <div className={'tooltip-container ' + (className ?? '')}> + {children} + <div className="tooltip"> + {text} + </div> + </div> + ); +} \ No newline at end of file diff --git a/client/src/components/ui/Tooltip/tooltip.scss b/client/src/components/ui/Tooltip/tooltip.scss new file mode 100644 index 0000000000000000000000000000000000000000..4e144c520b914f052052a71f0328ec8cd306976f --- /dev/null +++ b/client/src/components/ui/Tooltip/tooltip.scss @@ -0,0 +1,27 @@ +@use 'styles/settings.scss' as s; + +.tooltip-container { + position: relative; + z-index: 200; + &:hover { + .tooltip { + visibility: visible; + opacity: 1; + + } + } + + .tooltip { + visibility: hidden; + position: absolute; + opacity: 0; + transform: translate(-50%, -100%); + top: -5px; + left: 50%; + padding: 20px; + background: s.$white; + max-width: 300px; + border-radius: 5px; + box-shadow: 0 0 15px rgba(s.$black, 0.2); + } +} diff --git a/client/src/config.ts b/client/src/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd15358f0b3a31f36a7a5d1dc4d395a6af9aec53 --- /dev/null +++ b/client/src/config.ts @@ -0,0 +1,3 @@ + +export const apiRoot = 'http://localhost:8000/v1'; + diff --git a/client/src/images/svg/nav-bg.svg b/client/src/images/svg/nav-bg.svg deleted file mode 100644 index f9b758d1d2d4fd62709122e286017fd8b99984c5..0000000000000000000000000000000000000000 --- a/client/src/images/svg/nav-bg.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg width="411" height="78" viewBox="0 0 411 78" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g filter="url(#filter0_b)"> -<path d="M208.5 1.00001C35.5 -4.4826 -0.5 25.5 -0.5 25.5V77.5H411V27C411 27 381.5 6.48262 208.5 1.00001Z" fill="white"/> -</g> -</svg> diff --git a/client/src/index.scss b/client/src/index.scss index 46b3eb5ab99157beba7219048888796cfca70a7b..23c64a72db976f18a48ddfeb46d2cdec7b0df65a 100644 --- a/client/src/index.scss +++ b/client/src/index.scss @@ -1,6 +1,5 @@ - @use 'styles/settings'as s; -@use 'styles/mixins' as mx; +@use 'styles/mixins'as mx; @use 'styles/functions'as fn; * { @@ -18,30 +17,26 @@ html { body { color: s.$body-color; position: relative; - background: linear-gradient(to right, #fff 0%, #f2f2f2 100%); + background: s.$light; +} - @include mx.breakpoint(xlarge) { - padding: 5rem 0; - } +button { + background: none; + border: none; + outline: none; } -a { - font-weight: s.$weight-bold; +a, button { + font-weight: s.$weight-semi-bold; color: s.$primary; text-decoration: none; + cursor: pointer; &:hover { color: rgba(s.$primary, 0.5); } } -.page-container { - max-width: 1440px; - margin: 0 auto; - background: rgba(255, 255, 255, 0.4); - backdrop-filter: blur(30px); -} - .content-container { max-width: 1280px; width: 100%; @@ -49,17 +44,46 @@ a { padding: 30px; } -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { font-weight: s.$weight-bold; } h1 { font-size: fn.toRem(36); margin-bottom: fn.toRem(20); + font-weight: s.$weight-xbold; + + &.underlined { + position: relative; + display: inline-block; + + &:after { + content: ' '; + position: absolute; + right: -7px; + bottom: 5px; + width: 90px; + background: rgba(s.$secondary, .5); + height: 20px; + z-index: -1; + + @include mx.breakpoint(large) { + height: 24px; + width: 120px; + bottom: 10px; + } + + } + } @include mx.breakpoint(large) { font-size: fn.toRem(48); - margin-bottom: fn.toRem(40); + margin-bottom: fn.toRem(28); } } @@ -69,9 +93,7 @@ h2 { @include mx.breakpoint(large) { font-size: fn.toRem(36); - margin: 0 auto fn.toRem(24) auto; - max-width: 560px; - text-align: center; + margin-bottom: fn.toRem(24); } &.left { @@ -81,8 +103,9 @@ h2 { } h3 { - font-size: fn.toRem(20); + font-size: fn.toRem(18); margin-bottom: fn.toRem(12); + font-weight: s.$weight-semi-bold; @include mx.breakpoint(large) { font-size: fn.toRem(24); @@ -100,3 +123,41 @@ h4 { } } + +.background-container { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + z-index: -1; + overflow: hidden; + + .bubble { + width: 400px; + height: 400px; + position: absolute; + filter: blur(200px); + opacity: 0.9; + + @include mx.breakpoint(medium) { + width: 400px; + height: 400px; + filter: blur(400px); + } + + @include mx.breakpoint(large) { + width: 600px; + height: 600px; + } + + &.secondary { + background: s.$secondary; + } + + &.primary { + background: s.$primary; + } + + } +} diff --git a/client/src/pages/AppWrapper.tsx b/client/src/pages/AppWrapper.tsx new file mode 100644 index 0000000000000000000000000000000000000000..789a958531a3fe632726128b89b2e68b16b7cd99 --- /dev/null +++ b/client/src/pages/AppWrapper.tsx @@ -0,0 +1,28 @@ + +import { Suspense, lazy } from 'react'; + +import ProtectedRoute from 'components/helpers/ProtectedRoute'; +import Header from 'components/ui/Header'; + +const Tasks = lazy(() => import('pages/Tasks')); +const TaskDetail = lazy(() => import('pages/Tasks/TaskDetail')); +const Projects = lazy(() => import('pages/Projects')); +const Stats = lazy(() => import('pages/Stats')); +const Teams = lazy(() => import('pages/Teams')); +const Settings = lazy(() => import('pages/Settings')); + +export default function AppWrapper() { + return (<> + <Header> + <Suspense fallback={false}> + <ProtectedRoute path="/tasks/:uuid" component={TaskDetail} /> + <ProtectedRoute path="/tasks" exact component={Tasks} /> + <ProtectedRoute path="/projects" component={Projects} /> + <ProtectedRoute path="/stats" component={Stats} /> + <ProtectedRoute path="/teams" component={Teams} /> + <ProtectedRoute path="/settings" component={Settings} /> + </Suspense> + </Header> + </>); +} + diff --git a/client/src/pages/Home/home.scss b/client/src/pages/Home/home.scss index a1681c5d3dee9a42c1a5c48aad26c1056387a8ae..3e5f427795993596b2e33be5efcfbb9355cf91d3 100644 --- a/client/src/pages/Home/home.scss +++ b/client/src/pages/Home/home.scss @@ -1,472 +1,439 @@ - @use 'styles/settings.scss'as s; @use 'styles/mixins.scss'as mx; @use 'styles/functions.scss'as fn; -//General - -section { - margin-bottom: 70px; - @include mx.breakpoint(large) { - margin-bottom: 180px; - } +.landing-page { + //General - &.darker { - padding: 30px 0; - color: #f1f1f1; - background: linear-gradient(180deg, #2E313B 0.01%, #0C0E18 100%); + h2 { @include mx.breakpoint(large) { - margin: 0 75px; - padding: 75px 0; - border-radius: 25px; + margin: 0 auto fn.toRem(24) auto; + max-width: 560px; + text-align: center; } } -} - -// Hero section -header { - display: flex; - align-items: center; - padding: 20px; - @include mx.breakpoint(500) { - padding-top: 80px; - } + section { + margin-bottom: 70px; - @include mx.breakpoint(large) { - width: 30%; - padding-bottom: 160px; - } - - nav { - display: flex; + @include mx.breakpoint(large) { + margin-bottom: 180px; + } - a { - color: s.$body-color; - font-weight: s.$weight-bold; - margin-left: 40px; - display: none; + &.darker { + padding: 30px 0; + color: #f1f1f1; + background: linear-gradient(180deg, #2E313B 0.01%, #0C0E18 100%); - @include mx.breakpoint(medium) { - display: block; + @include mx.breakpoint(large) { + margin: 0 75px; + padding: 75px 0; + border-radius: 25px; } } } -} -.hero-section { - position: relative; + // Hero section - .hero-container { + header { display: flex; - flex-direction: column; - justify-content: space-between; + align-items: center; + padding: 20px; - @include mx.breakpoint(medium) { - height: calc(100vh - 0.5rem); + @include mx.breakpoint(500) { + padding-top: 80px; } - @include mx.breakpoint(xlarge) { - height: calc(100vh - 6rem); + @include mx.breakpoint(large) { + width: 30%; + padding-bottom: 160px; } - .text-container { - position: relative; - z-index: 2; - padding-bottom: 30px; - font-size: fn.toRem(18); + nav { + display: flex; - @include mx.breakpoint(500) { - width: 50%; + a { + color: s.$body-color; + font-weight: s.$weight-bold; + margin-left: 40px; + display: none; + + @include mx.breakpoint(medium) { + display: block; + } } + } + } + + .hero-section { + position: relative; + + .hero-container { + display: flex; + flex-direction: column; + justify-content: space-between; @include mx.breakpoint(medium) { - width: 30%; - padding-bottom: 160px; + height: calc(100vh - 0.5rem); } - .button-container { - margin-top: 40px; + @include mx.breakpoint(xlarge) { + height: calc(100vh - 6rem); } - } - .preview-container { - width: 50%; - display: flex; - justify-content: center; - margin: 20px 0; - padding: 30px 0; - border-top-right-radius: 25px; - border-bottom-right-radius: 25px; - background: s.$linear-gradient; - - @include mx.breakpoint(500) { - right: 0; - height: 100%; - width: 40%; - top: 0; - border-bottom-left-radius: 50px; - position: absolute; - margin: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - } + .text-container { + position: relative; + z-index: 2; + padding-bottom: 30px; + font-size: fn.toRem(18); - @include mx.breakpoint(large) { - width: 45%; + @include mx.breakpoint(500) { + width: 50%; + } + + @include mx.breakpoint(medium) { + width: 30%; + padding-bottom: 160px; + } + + .button-container { + margin-top: 40px; + } } - .preview-phone { - transform: translate(50%, 0%); - width: 75vw; + .preview-container { + width: 50%; + display: flex; + justify-content: center; + margin: 20px 0; + padding: 30px 0; + border-top-right-radius: 25px; + border-bottom-right-radius: 25px; + background: s.$linear-gradient; @include mx.breakpoint(500) { - left: 0; - top: 50%; - transform: translate(-20%, -50%); + right: 0; + height: 100%; + width: 40%; + top: 0; + border-bottom-left-radius: 50px; position: absolute; - width: 200px; + margin: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; } - @include mx.breakpoint(medium) { - left: 0; - transform: translate(-30%, -50%); - top: 50%; - width: 300px; + @include mx.breakpoint(large) { + width: 45%; } - .inner { - padding-bottom: 177.77777777778%; - background: url('/images/screen-design.jpg'); - background-size: 100% auto; - background-repeat: no-repeat; - background-position: top left; - width: 100%; - border: 7px solid #303030; - border-radius: 25px; - box-shadow: 0 0 40px rgba(0, 0, 0, .15); + .preview-phone { + transform: translate(50%, 0%); + width: 75vw; + + @include mx.breakpoint(500) { + left: 0; + top: 50%; + transform: translate(-20%, -50%); + position: absolute; + width: 200px; + } @include mx.breakpoint(medium) { - border-width: 10px; + left: 0; + transform: translate(-30%, -50%); + top: 50%; + width: 300px; } - &:before { - content: ' '; - position: absolute; - width: 25%; - height: 20px; - background: #303030; - left: 50%; - top: 2px; - border-radius: 5px; - transform: translateX(-50%); + .inner { + padding-bottom: 177.77777777778%; + background: url('/images/screen-design.jpg'); + background-size: 100% auto; + background-repeat: no-repeat; + background-position: top left; + width: 100%; + border: 7px solid #303030; + border-radius: 25px; + box-shadow: 0 0 40px rgba(0, 0, 0, .15); + + @include mx.breakpoint(medium) { + border-width: 10px; + } + + &:before { + content: ' '; + position: absolute; + width: 25%; + height: 20px; + background: #303030; + left: 50%; + top: 2px; + border-radius: 5px; + transform: translateX(-50%); + } } } } } - } -} + } -// Intro Section + // Intro Section -.intro-section { - .intro-container { - @include mx.breakpoint(medium) { - display: flex; - align-items: center; + .intro-section { + .intro-container { + @include mx.breakpoint(medium) { + display: flex; + align-items: center; + } } - } - .text-container { - padding-right: 10%; - margin-bottom: 50px; + .text-container { + padding-right: 10%; + margin-bottom: 50px; - @include mx.breakpoint(medium) { - flex-grow: 1; - margin-bottom: 0; + @include mx.breakpoint(medium) { + flex-grow: 1; + margin-bottom: 0; + } } - } - .preview-container { - display: flex; - justify-content: center; - align-items: center; - background: rgba(255, 255, 255, .5); - border-radius: 25px; - padding: 40px; + .preview-container { + display: flex; + justify-content: center; + align-items: center; + background: rgba(255, 255, 255, .5); + border-radius: 25px; + padding: 40px; - @include mx.breakpoint(large) { - padding: 110px; + @include mx.breakpoint(large) { + padding: 110px; + } } - } - .project-overview { - display: grid; - grid-gap: 20px; - grid-template-areas: - 'small-1 large' - 'small-2 large'; + .project-overview { + display: grid; + grid-gap: 20px; + grid-template-areas: + 'small-1 large' + 'small-2 large'; - @include mx.breakpoint(medium) { - grid-gap: 30px; + @include mx.breakpoint(medium) { + grid-gap: 30px; - .project-small { - .project { - width: 150px; - height: 150px; + .project-small { + .project { + width: 150px; + height: 150px; + } } - } - .project-large { - .project { - width: 150px; - height: 330px; + .project-large { + .project { + width: 150px; + height: 330px; + } } } - } - .small-1 { - grid-area: small-1; - } + .small-1 { + grid-area: small-1; + } - .small-2 { - grid-area: small-2; + .small-2 { + grid-area: small-2; - .project { - animation-delay: 3s; + .project { + animation-delay: 3s; + } } - } - .large { - grid-area: large; + .large { + grid-area: large; - .project { - animation-delay: 6s; + .project { + animation-delay: 6s; + height: 100%; + } } - } - .project { - animation: move-up 9s ease-in infinite; + .project { + animation: move-up 9s ease-in infinite; - svg { - width: 70px; - height: 70px; - } + svg { + width: 70px; + height: 70px; + } - @keyframes move-up { + @keyframes move-up { - 5%, - 35% { - transform: translateY(0); - box-shadow: 0px 5px 25px rgba(0, 0, 0, 0.05); - } + 5%, + 35% { + transform: translateY(0); + box-shadow: 0px 5px 25px rgba(0, 0, 0, 0.05); + } - 10%, - 30% { - transform: translateY(-10px); - box-shadow: 0px 5px 30px rgba(0, 0, 0, 0.15); + 10%, + 30% { + transform: translateY(-10px); + box-shadow: 0px 5px 30px rgba(0, 0, 0, 0.15); + } } - } + } } } -} - -//Feature section - -.feature-section { - .feature-container { - display: flex; - flex-direction: column; - justify-content: center; - margin: 5rem auto; - } - .feature-list { - margin-top: 10px; + //Feature section - @include mx.breakpoint(medium) { + .feature-section { + .feature-container { display: flex; + flex-direction: column; justify-content: center; + margin: 5rem auto; } - } - .feature-item { - display: flex; - flex-direction: column; - width: 100%; - padding: 1.5rem 0; + .feature-list { + margin-top: 10px; - @include mx.breakpoint(medium) { - padding: 20px; + @include mx.breakpoint(medium) { + display: flex; + justify-content: center; + } } - } - .feature-icon { - background: s.$radial-gradient; - background-clip: text; - -webkit-background-clip: text; - color: transparent; - font-size: 5rem; - } + .feature-item { + display: flex; + flex-direction: column; + width: 100%; + padding: 1.5rem 0; - .feature-title { - margin: 1rem 0; - } -} + @include mx.breakpoint(medium) { + padding: 20px; + } + } -// Team section + .feature-icon { + background: s.$radial-gradient; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + font-size: 5rem; + } -.team-section { - .team-container { - @include mx.breakpoint(large) { - display: flex; - flex-direction: column; - align-items: center; + .feature-title { + margin: 1rem 0; } } - .team-list { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - margin-top: 20px; - } + // Team section - .team-member { - text-align: left; - margin: 3rem 0; + .team-section { + .team-container { + @include mx.breakpoint(large) { + display: flex; + flex-direction: column; + align-items: center; + } + } - @include mx.breakpoint(medium) { + .team-list { display: flex; + flex-direction: column; align-items: center; justify-content: center; + margin-top: 20px; } - .team-member-title { - font-size: fn.toRem(14); - font-weight: s.$weight-bold; - display: inline-block; - padding: 2px 10px; - text-transform: uppercase; - background: radial-gradient(100% 1892.25% at 0% 0%, #AC42FF 0%, #8A24DA 100%); - letter-spacing: 0.5px; - margin-bottom: 10px; - color: s.$white; - border-radius: 5px; - } - - .team-member-image { - margin-right: 0; - margin-bottom: 3rem; + .team-member { + text-align: left; + margin: 3rem 0; @include mx.breakpoint(medium) { - margin-right: 3rem; + display: flex; + align-items: center; + justify-content: center; + } + + .team-member-title { + font-size: fn.toRem(14); + font-weight: s.$weight-bold; + display: inline-block; + padding: 2px 10px; + text-transform: uppercase; + background: radial-gradient(100% 1892.25% at 0% 0%, #AC42FF 0%, #8A24DA 100%); + letter-spacing: 0.5px; + margin-bottom: 10px; + color: s.$white; + border-radius: 5px; + } + + .team-member-image { + margin-right: 0; + margin-bottom: 3rem; border-radius: 50%; + + @include mx.breakpoint(medium) { + margin-right: 3rem; + margin-bottom: 0; + } } } } -} - -.contact-section { - .contact-container { - display: flex; - flex-direction: column; - text-align: center; - } - .contact-form { - margin-top: 2rem; - display: flex; - width: 100%; - flex-wrap: wrap; - max-width: 960px; - margin-left: auto; - margin-right: auto; - - .submit-button { - appearance: none; - border: none; - margin: 0.5rem; + .contact-section { + .contact-container { + display: flex; + flex-direction: column; + text-align: center; } - .button-container { + .contact-form { + margin-top: 2rem; display: flex; width: 100%; - justify-content: flex-end; - } - } -} + flex-wrap: wrap; + max-width: 960px; + margin-left: auto; + margin-right: auto; -footer { - .footer-container { - padding: 100px 0; - } - - .footer-copyright { - flex: 1 1 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - } - - .logo { - margin-bottom: 10px; - } - - .footer-nav { - flex: 1 1 100%; - display: flex; - justify-content: center; - margin-top: 30px; - - a { - padding: 5px 10px; } } -} - -.background-container { - position: absolute; - height: 100%; - width: 100%; - left: 0; - top: 0; - z-index: -1; - overflow: hidden; - - .bubble { - width: 400px; - height: 400px; - position: absolute; - filter: blur(200px); - opacity: 0.75; - @include mx.breakpoint(large) { - width: 500px; - height: 500px; - filter: blur(200px); + footer { + .footer-container { + padding: 100px 0; } - &.secondary { - background: s.$secondary; + .footer-copyright { + flex: 1 1 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } - &.primary { - background: s.$primary; + .logo { + margin-bottom: 10px; } - &.accent { - background: s.$accent; + .footer-nav { + flex: 1 1 100%; + display: flex; + justify-content: center; + margin-top: 30px; + + a { + padding: 5px 10px; + } } } -} +} \ No newline at end of file diff --git a/client/src/pages/Home/index.tsx b/client/src/pages/Home/index.tsx index 3389eb88fcd25a10854f43891ecec7e5a2390d87..6574e5ff8fd367a50c54979906378b04b71ef7be 100644 --- a/client/src/pages/Home/index.tsx +++ b/client/src/pages/Home/index.tsx @@ -1,7 +1,7 @@ import Project from 'components/ui/Project'; import ButtonLink from 'components/ui/ButtonLink'; -import Button from 'components/ui/Button'; -import TextInput from 'components/ui/TextInput'; +import ContactForm from 'components/forms/ContactForm'; +import Page from 'components/ui/Page'; import './home.scss'; import Logo from 'images/logo.svg'; @@ -10,158 +10,147 @@ import ImageDaniel from 'images/daniel-planoetscher.jpg'; export default function Home() { return (<> - <div className="page-container"> - <main> - <section className="hero-section" id="hero"> - <div className="hero-container"> - <header> - <a href="index.html"> - <img src={Logo} alt="Logo" width="100" height="35" /> - </a> - <nav> - <a href="#hero">Home</a> - <a href="#team">Team</a> - <a href="#contact">Contact</a> - </nav> - </header> - <div className="preview-container"> - <div className="preview-phone"> - <div className="inner"> - </div> + <Page className="landing-page"> + <section className="hero-section" id="hero"> + <div className="hero-container"> + <header> + <a href="index.html"> + <img src={Logo} alt="Logo" width="100" height="35" /> + </a> + <nav> + <a href="#hero">Home</a> + <a href="#team">Team</a> + <a href="#contact">Contact</a> + </nav> + </header> + <div className="preview-container"> + <div className="preview-phone"> + <div className="inner"> </div> </div> - <div className="content-container"> - <div className="text-container"> - <h1>ryoko</h1> - <p>Are you feeling lost with your tasks? Maximize your productivity now with ryoko.</p> - <div className="button-container"> - <ButtonLink href="/register" routing={true}>Get started</ButtonLink> - </div> + </div> + <div className="content-container"> + <div className="text-container"> + <h1>ryoko</h1> + <p>Are you feeling lost with your tasks? Maximize your productivity now with ryoko.</p> + <div className="button-container"> + <ButtonLink href="/tasks" routing={true}>Get started</ButtonLink> </div> </div> </div> - </section> - <section className="intro-section" id="intro"> - <div className="content-container"> - <div className="intro-container"> - <div className="text-container"> - <h2 className="left">Plan your projects like a journey!</h2> - <p> - Do you want to boost your productivity and agility of your + </div> + </section> + <section className="intro-section" id="intro"> + <div className="content-container"> + <div className="intro-container"> + <div className="text-container"> + <h2 className="left">Plan your projects like a journey!</h2> + <p> + Do you want to boost your productivity and agility of your development? <br /> With ryoko you are able to effectively plan your tasks and manage your projects. It is build with developers in mind and facilitates effective collaboration. </p> - </div> - <div className="preview-container"> - <div className="project-overview"> - <div className="small-1 project-small"> - <Project status="open" name="Hello world!" percent={5} /> - </div> - <div className="small-2 project-small"> - <Project status="suspended" name="FizzBuzz" percent={33} /> - </div> - <div className="large project-large"> - <Project status="open" name="Array summation" percent={78} /> - </div> + </div> + <div className="preview-container"> + <div className="project-overview"> + <div className="small-1 project-small"> + <Project status="open" name="Hello world!" percent={5} /> + </div> + <div className="small-2 project-small"> + <Project status="suspended" name="FizzBuzz" percent={33} /> + </div> + <div className="large project-large"> + <Project status="open" name="Array summation" percent={78} /> </div> </div> </div> </div> - </section> - <section className="feature-section"> - <div className="content-container feature-container"> - <h2>Revolutionize your productivity</h2> - <div className="feature-list"> - <div className="feature-item"> - <span className="feature-icon material-icons">query_stats</span> - <h3 className="feature-title">Analyze your productivity</h3> - <div className="feature-description"> - Find your <strong>weaknesses and strengths</strong> by analyzing graphs - </div> + </div> + </section> + <section className="feature-section"> + <div className="content-container feature-container"> + <h2>Revolutionize your productivity</h2> + <div className="feature-list"> + <div className="feature-item"> + <span className="feature-icon material-icons">query_stats</span> + <h3 className="feature-title">Analyze your productivity</h3> + <div className="feature-description"> + Find your <strong>weaknesses and strengths</strong> by analyzing graphs </div> - <div className="feature-item"> - <span className="feature-icon material-icons">event</span> - <h3 className="feature-title">Automatic timetables</h3> - <div className="feature-description"> - Generate your automatic timetables based on <strong>priorities and depencies</strong> of + </div> + <div className="feature-item"> + <span className="feature-icon material-icons">event</span> + <h3 className="feature-title">Automatic timetables</h3> + <div className="feature-description"> + Generate your automatic timetables based on <strong>priorities and depencies</strong> of your tasks </div> - </div> - <div className="feature-item"> - <span className="feature-icon material-icons">group</span> - <h3 className="feature-title">Teambased</h3> - <div className="feature-description"> - Distribute task within your Teams based on <strong>profession and difficulty</strong> - </div> + </div> + <div className="feature-item"> + <span className="feature-icon material-icons">group</span> + <h3 className="feature-title">Teambased</h3> + <div className="feature-description"> + Distribute task within your Teams based on <strong>profession and difficulty</strong> </div> </div> </div> - </section> - <section className="team-section" id="team"> - <div className="content-container team-container"> - <h2>Our Team</h2> - <p> - People are what makes a project great, and here are the people that make us - great. + </div> + </section> + <section className="team-section" id="team"> + <div className="content-container team-container"> + <h2>Our Team</h2> + <p> + People are what makes a project great, and here are the people that make us + great. </p> - <div className="team-list"> - <div className="team-member"> - <img className="team-member-image" src={ImageDaniel} width="200" height="200" - alt="Daniel Planötscher" /> - <div className="team-member-info"> - <div className="team-member-title">Web Developer</div> - <h3 className="team-member-name">Daniel Planötscher</h3> - <div className="team-member-description"> - Besides studying Computer Science, Daniel also enjoys taking photos and - designing user interfaces for hobby projects, which is why he focuses on - the FrontEnd of ryoko. Furthermore, he brings significant industry - experience working as a web developer using state of the art tools and - programming techniques. He is involved in the creation of modern - websites for dozens of clients with advanced requirements. - </div> + <div className="team-list"> + <div className="team-member"> + <img className="team-member-image" src={ImageDaniel} width="200" height="200" + alt="Daniel Planötscher" /> + <div className="team-member-info"> + <div className="team-member-title">Web Developer</div> + <h3 className="team-member-name">Daniel Planötscher</h3> + <div className="team-member-description"> + Besides studying Computer Science, Daniel also enjoys taking photos and + designing user interfaces for hobby projects, which is why he focuses on + the FrontEnd of ryoko. Furthermore, he brings significant industry + experience working as a web developer using state of the art tools and + programming techniques. He is involved in the creation of modern + websites for dozens of clients with advanced requirements. </div> </div> - <div className="team-member"> - <img className="team-member-image" src={ImageRoland} width="200" height="200" - alt="Roland Bernard" /> - <div className="team-member-info"> - <h4 className="team-member-title">Software Engineer</h4> - <h3 className="team-member-name">Roland Bernard</h3> - <div className="team-member-description"> - Studying Computer Science and participating in Competitive Programming - Competitions, Roland constitutes a integral part of our development team - at ryoko. Like all members of our team he also has experience in the - industry, mainly working on business management systems and software for - the financial sector. He is also experienced in the implementation of - development tools and infrastructure components. - </div> + </div> + <div className="team-member"> + <img className="team-member-image" src={ImageRoland} width="200" height="200" + alt="Roland Bernard" /> + <div className="team-member-info"> + <h4 className="team-member-title">Software Engineer</h4> + <h3 className="team-member-name">Roland Bernard</h3> + <div className="team-member-description"> + Studying Computer Science and participating in Competitive Programming + Competitions, Roland constitutes a integral part of our development team + at ryoko. Like all members of our team he also has experience in the + industry, mainly working on business management systems and software for + the financial sector. He is also experienced in the implementation of + development tools and infrastructure components. </div> </div> </div> </div> - </section> - <section className="contact-section darker" id="contact"> - <div className="content-container contact-container"> - <h2>Get in touch</h2> - <p> - Do you still have a question? Just contact us directly and we will be glad - to help you resolve the issue. + </div> + </section> + <section className="contact-section darker" id="contact"> + <div className="content-container contact-container"> + <h2>Get in touch</h2> + <p> + Do you still have a question? Just contact us directly and we will be glad + to help you resolve the issue. </p> - <form className="contact-form" action="mailto:dplanoetscher@unibz.it" method="GET"> - <TextInput label="Fistname" name="firstname" /> - <TextInput label="Lastname" name="lastname" /> - <TextInput label="Email" name="email" type="email" /> - <TextInput label="Subject" name="subject" /> - <TextInput label="Message" name="message" type="textarea" /> - <div className="button-container"> - <Button type="submit">Send</Button> - </div> - </form> - </div> - </section> - </main> + <ContactForm /> + </div> + </section> <footer> <div className="content-container footer-container"> <div className="footer-copyright"> @@ -180,14 +169,14 @@ export default function Home() { </div> </div> </footer> - </div> + </Page> <div className="background-container"> <div className="bubble secondary" style={{ top: '0', right: '0' }}></div> <div className="bubble primary" style={{ top: '20%', left: '0' }}></div> - <div className="bubble accent" style={{ top: '32%', right: '5%' }}></div> + <div className="bubble secondary" style={{ top: '32%', right: '5%' }}></div> <div className="bubble primary" style={{ top: '50%', right: '20%' }}></div> <div className="bubble secondary" style={{ top: '65%', left: '-15%' }}></div> - <div className="bubble accent" style={{ bottom: '5%', left: '10%' }}></div> + <div className="bubble secondary" style={{ bottom: '5%', left: '10%' }}></div> <div className="bubble primary" style={{ bottom: '0%', right: '0%' }}></div> </div> </>); diff --git a/client/src/pages/Login/index.tsx b/client/src/pages/Login/index.tsx index bcaa2071ae578bb5278236c6786885d9bf8b7038..5bb4ba9971125ef7996dc4641f6ab04a5629aa75 100644 --- a/client/src/pages/Login/index.tsx +++ b/client/src/pages/Login/index.tsx @@ -1,9 +1,35 @@ - +import Page from 'components/ui/Page'; +import { Link, useHistory } from 'react-router-dom'; +import LoginForm from 'components/forms/LoginForm'; import './login.scss'; +import { useCallback, useState } from 'react'; +import { login } from 'adapters/auth'; export default function Login() { + const history = useHistory(); + const [error, setError] = useState<string>(''); + const handleSubmit = useCallback(async (username: string, password: string) => { + try { + if (await login(username, password)) { + history.push('/tasks'); + } + } catch (e) { } + }, [history]); + return ( - <div> + <div className="login-page-container"> + <Page className="login-page"> + + <div className="content-container"> + <h1 className="underlined">Login</h1> + <LoginForm onSubmit={handleSubmit} /> + <Link to="/register" className="link">You don't have an account?</Link> + </div> + </Page> + <div className="background-container"> + <div className="bubble primary" style={{ top: '0', right: '0' }}></div> + <div className="bubble secondary" style={{ bottom: '-20%', left: '-20%' }}></div> + </div> </div> ); } diff --git a/client/src/pages/Login/login.scss b/client/src/pages/Login/login.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d83f594bb850587d16abbfc314634d28472ebc74 100644 --- a/client/src/pages/Login/login.scss +++ b/client/src/pages/Login/login.scss @@ -0,0 +1,30 @@ +@use 'styles/settings.scss'as s; +@use 'styles/mixins.scss'as mx; + +.login-page-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + + .login-page { + max-width: 600px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + @include mx.breakpoint(medium) { + min-height: auto; + padding: 60px; + border-radius: 25px; + } + + .link { + display: block; + text-decoration: underline; + margin-top: 20px; + text-align: center; + } + } +} diff --git a/client/src/pages/Projects/index.tsx b/client/src/pages/Projects/index.tsx index 0ad04b0e0b9476e3e8fef1b0426c4d9b7d7f832a..3dd1f6fe5b493f134ef34713552245efc4b9b961 100644 --- a/client/src/pages/Projects/index.tsx +++ b/client/src/pages/Projects/index.tsx @@ -1,14 +1,12 @@ - -import Navigation from 'components/ui/Navigation'; import './projects.scss'; +import Page from 'components/ui/Page'; export default function Tasks() { return ( - <div className="projects-page"> - <Navigation /> - <main> - <h1>Projects</h1> - </main> - </div> + <Page className="projects-page"> + <div className="content-container"> + <h1 className="underlined">Projects</h1> + </div> + </Page> ); -} \ No newline at end of file +} diff --git a/client/src/pages/Register/index.tsx b/client/src/pages/Register/index.tsx index ca7560d0d62520efffc4815ba0be6ec34930111f..e6b4a7fad350d995723600b7a8254ab734eb040e 100644 --- a/client/src/pages/Register/index.tsx +++ b/client/src/pages/Register/index.tsx @@ -1,10 +1,38 @@ +import { useCallback } from 'react'; +import { Link, useHistory } from 'react-router-dom'; + +import Page from 'components/ui/Page'; +import RegisterForm from 'components/forms/RegisterForm'; +import { register } from 'adapters/auth'; + import './register.scss'; export default function Register() { + const history = useHistory(); + + const handleSubmit = useCallback(async (username: string, password: string) => { + try { + if (await register(username, password)) { + history.push('/tasks'); + } + } catch (e) { } + }, [history]); + return ( - <div> - register + <div className="register-page-container"> + <Page className="register-page"> + <div className="content-container"> + <h1 className="underlined">Register</h1> + <RegisterForm onSubmit={handleSubmit} /> + <Link className="link" to="/login">You already have an account?</Link> + </div> + </Page> + <div className="background-container"> + <div className="bubble primary" style={{ top: '-10%', right: '-20%' }}></div> + <div className="bubble secondary" style={{ bottom: '-20%', left: '-20%' }}></div> + </div> </div> ); -} \ No newline at end of file +} + diff --git a/client/src/pages/Register/register.scss b/client/src/pages/Register/register.scss index 8b137891791fe96927ad78e64b0aad7bded08bdc..7894312608a9d9009f3f099350d67b56cea5c501 100644 --- a/client/src/pages/Register/register.scss +++ b/client/src/pages/Register/register.scss @@ -1 +1,30 @@ +@use 'styles/settings.scss'as s; +@use 'styles/mixins.scss'as mx; +.register-page-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100vh; + + .register-page { + max-width: 700px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + @include mx.breakpoint(medium) { + min-height: auto; + padding: 60px; + border-radius: 25px; + } + + .link { + display: block; + text-decoration: underline; + margin-top: 20px; + text-align: center; + } + } +} diff --git a/client/src/pages/Settings/index.tsx b/client/src/pages/Settings/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b2884b2bfe488de1cfa952e084445d4f4ff0446f --- /dev/null +++ b/client/src/pages/Settings/index.tsx @@ -0,0 +1,12 @@ +import './settings.scss'; +import Page from 'components/ui/Page'; + +export default function Settings() { + return ( + <Page className="settings-page"> + <div className="content-container"> + <h1 className="underlined">Settings</h1> + </div> + </Page> + ) +} \ No newline at end of file diff --git a/client/src/pages/Settings/settings.scss b/client/src/pages/Settings/settings.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/client/src/pages/Stats/index.tsx b/client/src/pages/Stats/index.tsx index 7a368a472572f10d8f080aeba621090d9575ebcf..713748c93bcde7e4e427342f708c9916f962ef6c 100644 --- a/client/src/pages/Stats/index.tsx +++ b/client/src/pages/Stats/index.tsx @@ -1,14 +1,12 @@ - -import Navigation from 'components/ui/Navigation'; +import Page from 'components/ui/Page'; import './stats.scss'; export default function Tasks() { return ( - <div className="stats-page"> - <Navigation /> - <main> - <h1>Stats</h1> - </main> - </div> + <Page className="stats-page"> + <div className="content-container"> + <h1 className="underlined">Stats</h1> + </div> + </Page> ); -} \ No newline at end of file +} diff --git a/client/src/pages/Tasks/TaskDetail/index.tsx b/client/src/pages/Tasks/TaskDetail/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fb4c11e486c155c75a37bcde9f4fcba226337e48 --- /dev/null +++ b/client/src/pages/Tasks/TaskDetail/index.tsx @@ -0,0 +1,13 @@ +import './task-detail.scss'; +import Page from 'components/ui/Page'; + +export default function TaskDetail() { + + return ( + <Page className="tasks-detail-page"> + <div className="content-container"> + <h1>Task</h1> + </div> + </Page> + ); +} \ No newline at end of file diff --git a/client/src/pages/Tasks/TaskDetail/task-detail.scss b/client/src/pages/Tasks/TaskDetail/task-detail.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/client/src/pages/Tasks/index.tsx b/client/src/pages/Tasks/index.tsx index 6e12d538b58dc19f9ac96dc2076d4737cb7bb82b..3732996667a4956ac049992fb1d691665f976741 100644 --- a/client/src/pages/Tasks/index.tsx +++ b/client/src/pages/Tasks/index.tsx @@ -1,14 +1,47 @@ +import Page from 'components/ui/Page'; +import Task from 'components/ui/Task'; import './tasks.scss'; export default function Tasks() { return ( <> - <div className="tasks-page page-container"> + <Page className="tasks-page"> <main className="content-container"> - <h1>Tasks</h1> - <p>Hey Daniel, you have <strong>10</strong> Tasks for today.</p> + <section className="intro-section"> + <h1 className="underlined">Tasks</h1> + <p>Hey Daniel, you have <strong>10 tasks</strong> for today.</p> + </section> + <section className="tasks-container"> + <h2>Today</h2> + <div className="task-group"> + <h3>09:00</h3> + <div className="tasks-list"> + <Task task={{ + uuid: 'asdf', + name: 'Create API Routes', + icon: '🌎', + start: 1619074800000, + end: 1619076600000, + description: 'Create the API routes and implement them into the FrontEnd, by adding them into the controls.' + }} /> + <Task task={{ + uuid: 'asdfds', + name: 'Create API Routes', + icon: '🌎', + start: 1619074800000, + end: 1619076600000, + description: 'Create the API routes and implement them into the FrontEnd, by adding them into the controls.' + }} /> + </div> + </div> + </section> </main> + </Page> + <div className="background-container"> + <div className="bubble primary" style={{ top: '-20%', right: '-20%' }}></div> + <div className="bubble secondary" style={{ bottom: '-20%', left: '20%' }}></div> </div> </> ); -} \ No newline at end of file +} + diff --git a/client/src/pages/Tasks/tasks.scss b/client/src/pages/Tasks/tasks.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d5192cb9ae305af44b2373542fbc0fc2fafbfe94 100644 --- a/client/src/pages/Tasks/tasks.scss +++ b/client/src/pages/Tasks/tasks.scss @@ -0,0 +1,33 @@ +@use 'styles/settings'as s; +@use 'styles/mixins'as mx; +@use 'styles/functions'as fn; + +.tasks-page { + .intro-section { + font-size: fn.toRem(18); + } + + .tasks-container { + margin-top: 50px; + + .task-group { + @include mx.breakpoint(medium) { + display: flex; + width: 100%; + + h3 { + width: 30%; + } + + .tasks-list { + flex-grow: 1; + } + } + + .task { + margin-bottom: 30px; + } + } + + } +} \ No newline at end of file diff --git a/client/src/pages/Teams/index.tsx b/client/src/pages/Teams/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..00b1e6a98b610c7a5a93621743b00fc64c17419a --- /dev/null +++ b/client/src/pages/Teams/index.tsx @@ -0,0 +1,12 @@ +import './teams.scss'; +import Page from 'components/ui/Page'; + +export default function Teams() { + return ( + <Page className="teams-page"> + <div className="content-container"> + <h1 className="underlined">Teams</h1> + </div> + </Page> + ) +} \ No newline at end of file diff --git a/client/src/pages/Teams/teams.scss b/client/src/pages/Teams/teams.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/client/src/service-worker.ts b/client/src/service-worker.ts index b714b8bc0432513cdff132e11f800e9170518688..2596bac005e729afb695c692b400c91e50812ea3 100644 --- a/client/src/service-worker.ts +++ b/client/src/service-worker.ts @@ -1,11 +1,11 @@ /// <reference lib="webworker" /> /* eslint-disable no-restricted-globals */ -import {clientsClaim} from 'workbox-core'; -import {ExpirationPlugin} from 'workbox-expiration'; -import {precacheAndRoute, createHandlerBoundToURL} from 'workbox-precaching'; -import {registerRoute} from 'workbox-routing'; -import {StaleWhileRevalidate} from 'workbox-strategies'; +import { clientsClaim } from 'workbox-core'; +import { ExpirationPlugin } from 'workbox-expiration'; +import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { StaleWhileRevalidate } from 'workbox-strategies'; declare const self: ServiceWorkerGlobalScope; @@ -45,11 +45,10 @@ registerRoute( createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') ); -// An example runtime caching route for requests that aren't handled by the -// precache, in this case same-origin .png requests like those from in public/ +// Runtime caching route for images requests that aren't handled by the precache. registerRoute( // Add in any other file extensions or routing criteria as needed. - ({url}) => url.origin === self.location.origin && url.pathname.endsWith('.png'), + ({url}) => url.origin === self.location.origin && url.pathname.match(/.(png|jpg|jpeg|gif)$/), // Customize this strategy as needed, e.g., by changing to CacheFirst. new StaleWhileRevalidate({ cacheName: 'images', diff --git a/client/src/styles/settings.scss b/client/src/styles/settings.scss index 0ecc96bde62d0e6f6c3d0021a5e142f05de8f0a0..3df68422a6d58c6273b9990fcc2851532d5ad62a 100644 --- a/client/src/styles/settings.scss +++ b/client/src/styles/settings.scss @@ -1,14 +1,17 @@ // Colors $primary: #AC42FF; -$secondary: #F15154; -$accent: #7DEFFF; -$light: #f0f0f0; -$dark: #1f1f1f; +$secondary: #7DEFFF; +$light: #F1F1F1; +$dark: #180923; + +$light-gray: #F8F8F8; $white: #fff; $black: #000; +$error-color: $secondary; + $colors: ( 'primary': $primary, 'secondary': $secondary, @@ -16,8 +19,8 @@ $colors: ( 'dark': $dark ); -$linear-gradient: linear-gradient(to top, $primary, $secondary); -$radial-gradient: radial-gradient(100% 115% at 0% 0%, $primary 0%, $secondary 100%); +$linear-gradient: linear-gradient(to bottom, $primary, #930AFF); +$radial-gradient: radial-gradient(100% 115% at 0% 0%, $primary 0%, #930AFF 100%); $body-color: #3A5255; $body-font: 'Poppins', sans-serif; @@ -28,6 +31,7 @@ $weight-regular: 400; $weight-medium: 500; $weight-semi-bold: 600; $weight-bold: 700; +$weight-xbold: 800; // Breakpoints $breakpoints: ( diff --git a/server/package.json b/server/package.json index a026e72ca9f6f208ec9530407babee3510be5b45..59765578637096588f872e99df522f8a09980311 100644 --- a/server/package.json +++ b/server/package.json @@ -8,8 +8,10 @@ "private": true, "main": "src/index.ts", "dependencies": { + "@types/cors": "^2.8.10", "bcrypt": "^5.0.1", "body-parser": "^1.19.0", + "cors": "^2.8.5", "express": "^4.17.1", "express-fileupload": "^1.2.1", "jsonwebtoken": "^8.5.1", diff --git a/server/src/index.ts b/server/src/index.ts index 3c6809c3f15b57899c12f1ee3925578e7716fc8f..cba9f525a5ed76cf2487dcea343e68a8b0a3e275 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -15,6 +15,7 @@ app.use(fileupload()); app.use('/v1', v1); + app.use((_req, res) => { res.status(404).json({ status: 'error', diff --git a/server/yarn.lock b/server/yarn.lock index ed1f5c9f1de47cfcdc813af470f0dba9175816eb..a9a0b168eec5d8fef27eb70a485634d3de757f02 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -9,25 +9,25 @@ dependencies: "@babel/highlight" "^7.12.13" -"@babel/compat-data@^7.13.12": - version "7.13.15" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.15.tgz#7e8eea42d0b64fda2b375b22d06c605222e848f4" - integrity sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA== +"@babel/compat-data@^7.13.15": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.14.0.tgz#a901128bce2ad02565df95e6ecbf195cf9465919" + integrity sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q== "@babel/core@^7.1.0", "@babel/core@^7.7.5": - version "7.13.15" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.13.15.tgz#a6d40917df027487b54312202a06812c4f7792d0" - integrity sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ== + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.14.0.tgz#47299ff3ec8d111b493f1a9d04bf88c04e728d88" + integrity sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.13.9" - "@babel/helper-compilation-targets" "^7.13.13" - "@babel/helper-module-transforms" "^7.13.14" - "@babel/helpers" "^7.13.10" - "@babel/parser" "^7.13.15" + "@babel/generator" "^7.14.0" + "@babel/helper-compilation-targets" "^7.13.16" + "@babel/helper-module-transforms" "^7.14.0" + "@babel/helpers" "^7.14.0" + "@babel/parser" "^7.14.0" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.15" - "@babel/types" "^7.13.14" + "@babel/traverse" "^7.14.0" + "@babel/types" "^7.14.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.2" @@ -35,21 +35,21 @@ semver "^6.3.0" source-map "^0.5.0" -"@babel/generator@^7.13.9": - version "7.13.9" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39" - integrity sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw== +"@babel/generator@^7.14.0": + version "7.14.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.1.tgz#1f99331babd65700183628da186f36f63d615c93" + integrity sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ== dependencies: - "@babel/types" "^7.13.0" + "@babel/types" "^7.14.1" jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-compilation-targets@^7.13.13": - version "7.13.13" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz#2b2972a0926474853f41e4adbc69338f520600e5" - integrity sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ== +"@babel/helper-compilation-targets@^7.13.16": + version "7.13.16" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz#6e91dccf15e3f43e5556dffe32d860109887563c" + integrity sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA== dependencies: - "@babel/compat-data" "^7.13.12" + "@babel/compat-data" "^7.13.15" "@babel/helper-validator-option" "^7.12.17" browserslist "^4.14.5" semver "^6.3.0" @@ -84,19 +84,19 @@ dependencies: "@babel/types" "^7.13.12" -"@babel/helper-module-transforms@^7.13.14": - version "7.13.14" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz#e600652ba48ccb1641775413cb32cfa4e8b495ef" - integrity sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g== +"@babel/helper-module-transforms@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz#8fcf78be220156f22633ee204ea81f73f826a8ad" + integrity sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw== dependencies: "@babel/helper-module-imports" "^7.13.12" "@babel/helper-replace-supers" "^7.13.12" "@babel/helper-simple-access" "^7.13.12" "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/helper-validator-identifier" "^7.12.11" + "@babel/helper-validator-identifier" "^7.14.0" "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.13" - "@babel/types" "^7.13.14" + "@babel/traverse" "^7.14.0" + "@babel/types" "^7.14.0" "@babel/helper-optimise-call-expression@^7.12.13": version "7.12.13" @@ -134,38 +134,38 @@ dependencies: "@babel/types" "^7.12.13" -"@babel/helper-validator-identifier@^7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" - integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" + integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== "@babel/helper-validator-option@^7.12.17": version "7.12.17" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831" integrity sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw== -"@babel/helpers@^7.13.10": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.13.10.tgz#fd8e2ba7488533cdeac45cc158e9ebca5e3c7df8" - integrity sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ== +"@babel/helpers@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.14.0.tgz#ea9b6be9478a13d6f961dbb5f36bf75e2f3b8f62" + integrity sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg== dependencies: "@babel/template" "^7.12.13" - "@babel/traverse" "^7.13.0" - "@babel/types" "^7.13.0" + "@babel/traverse" "^7.14.0" + "@babel/types" "^7.14.0" "@babel/highlight@^7.12.13": - version "7.13.10" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.13.10.tgz#a8b2a66148f5b27d666b15d81774347a731d52d1" - integrity sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg== + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" + integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== dependencies: - "@babel/helper-validator-identifier" "^7.12.11" + "@babel/helper-validator-identifier" "^7.14.0" chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.13.15": - version "7.13.15" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8" - integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ== +"@babel/parser@^7.1.0", "@babel/parser@^7.12.13", "@babel/parser@^7.14.0": + version "7.14.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.14.1.tgz#1bd644b5db3f5797c4479d89ec1817fe02b84c47" + integrity sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q== "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" @@ -260,27 +260,26 @@ "@babel/parser" "^7.12.13" "@babel/types" "^7.12.13" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.13.13", "@babel/traverse@^7.13.15": - version "7.13.15" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.15.tgz#c38bf7679334ddd4028e8e1f7b3aa5019f0dada7" - integrity sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.13.0", "@babel/traverse@^7.14.0": + version "7.14.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.14.0.tgz#cea0dc8ae7e2b1dec65f512f39f3483e8cc95aef" + integrity sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA== dependencies: "@babel/code-frame" "^7.12.13" - "@babel/generator" "^7.13.9" + "@babel/generator" "^7.14.0" "@babel/helper-function-name" "^7.12.13" "@babel/helper-split-export-declaration" "^7.12.13" - "@babel/parser" "^7.13.15" - "@babel/types" "^7.13.14" + "@babel/parser" "^7.14.0" + "@babel/types" "^7.14.0" debug "^4.1.0" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.0", "@babel/types@^7.13.12", "@babel/types@^7.13.14", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.13.14" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.13.14.tgz#c35a4abb15c7cd45a2746d78ab328e362cbace0d" - integrity sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ== +"@babel/types@^7.0.0", "@babel/types@^7.12.13", "@babel/types@^7.13.12", "@babel/types@^7.14.0", "@babel/types@^7.14.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.14.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.1.tgz#095bd12f1c08ab63eff6e8f7745fa7c9cc15a9db" + integrity sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA== dependencies: - "@babel/helper-validator-identifier" "^7.12.11" - lodash "^4.17.19" + "@babel/helper-validator-identifier" "^7.14.0" to-fast-properties "^2.0.0" "@bcoe/v8-coverage@^0.2.3": @@ -484,9 +483,9 @@ chalk "^4.0.0" "@mapbox/node-pre-gyp@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.3.tgz#c740c23ec1007b9278d4c28f767b6e843a88c3d3" - integrity sha512-9dTIfQW8HVCxLku5QrJ/ysS/b2MdYngs9+/oPrOTLvp3TrggdANYVW2h8FGJGDf0J7MYfp44W+c90cVJx+ASuA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.4.tgz#6c76e7a40138eac39e1a4dc869a083e43e236c00" + integrity sha512-M669Qo4nRT7iDmQEjQYC7RU8Z6dpz9UmSbkJ1OFEja3uevCdLKh7IZZki7L1TZj02kRyl82snXFY8QqkyfowrQ== dependencies: detect-libc "^1.0.3" https-proxy-agent "^5.0.0" @@ -577,6 +576,11 @@ dependencies: "@types/node" "*" +"@types/cors@^2.8.10": + version "2.8.10" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" + integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== + "@types/express-fileupload@^1.1.6": version "1.1.6" resolved "https://registry.yarnpkg.com/@types/express-fileupload/-/express-fileupload-1.1.6.tgz#fdc3b1d6f54fe4522867f8550f0acf24bcbf5c45" @@ -630,9 +634,9 @@ "@types/istanbul-lib-report" "*" "@types/jest@^26.0.22": - version "26.0.22" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.22.tgz#8308a1debdf1b807aa47be2838acdcd91e88fbe6" - integrity sha512-eeWwWjlqxvBxc4oQdkueW5OF/gtfSceKk4OnOAGlUSwS/liBRtZppbJuz1YkgbrbfGOoeBHun9fOvXnjNwrSOw== + version "26.0.23" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.23.tgz#a1b7eab3c503b80451d019efb588ec63522ee4e7" + integrity sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA== dependencies: jest-diff "^26.0.0" pretty-format "^26.0.0" @@ -649,10 +653,15 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== -"@types/node@*", "@types/node@^14.14.37": - version "14.14.37" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" - integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== +"@types/node@*": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a" + integrity sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA== + +"@types/node@^14.14.37": + version "14.14.43" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8" + integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ== "@types/normalize-package-data@^2.4.0": version "2.4.0" @@ -748,9 +757,9 @@ acorn@^7.1.1: integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== acorn@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.0.tgz#52311fd7037ae119cbb134309e901aa46295b3fe" - integrity sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA== + version "8.2.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.2.tgz#c4574e4fea298d6e6ed4b85ab844b06dd59f26d6" + integrity sha512-VrMS8kxT0e7J1EX0p6rI/E0FbfOVcvBpbIqHThFv+f8YrZIlMfVotYcXKVPmTvPW8sW5miJzfUFrrvthUZg8VQ== agent-base@6: version "6.0.2" @@ -1107,15 +1116,15 @@ browser-process-hrtime@^1.0.0: integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== browserslist@^4.14.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== + version "4.16.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" + integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" + caniuse-lite "^1.0.30001219" + colorette "^1.2.2" + electron-to-chromium "^1.3.723" escalade "^3.1.1" - node-releases "^1.1.70" + node-releases "^1.1.71" bs-logger@0.x: version "0.2.6" @@ -1204,10 +1213,10 @@ camelcase@^6.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001181: - version "1.0.30001208" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz#a999014a35cebd4f98c405930a057a0d75352eb9" - integrity sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA== +caniuse-lite@^1.0.30001219: + version "1.0.30001221" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001221.tgz#b916721ddf59066cfbe96c5c9a77cf7ae5c52e65" + integrity sha512-b9TOZfND3uGSLjMOrLh8XxSQ41x8mX+9MLJYDM4AAHLfaZHttrLNPrScWjVnBITRZbY5sPpCt7X85n7VSLZ+/g== capture-exit@^2.0.0: version "2.0.0" @@ -1239,9 +1248,9 @@ chalk@^3.0.0: supports-color "^7.1.0" chalk@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" - integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + version "4.1.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" + integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" @@ -1385,7 +1394,7 @@ colorette@1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -colorette@^1.2.1: +colorette@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== @@ -1468,6 +1477,14 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -1709,10 +1726,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.649: - version "1.3.710" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.710.tgz#b33d316e5d6de92b916e766d8a478d19796ffe11" - integrity sha512-b3r0E2o4yc7mNmBeJviejF1rEx49PUBi+2NPa7jHEX3arkAXnVgLhR0YmV8oi6/Qf3HH2a8xzQmCjHNH0IpXWQ== +electron-to-chromium@^1.3.723: + version "1.3.725" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.725.tgz#04fc83f9189169aff50f0a00c6b4090b910cba85" + integrity sha512-2BbeAESz7kc6KBzs7WVrMc1BY5waUphk4D4DX5dSQXJhsc3tP5ZFaiyuL0AB7vUKzDYpIeYwTYlEfxyjsGUrhw== emittery@^0.7.1: version "0.7.2" @@ -2489,9 +2506,9 @@ is-ci@^2.0.0: ci-info "^2.0.0" is-core-module@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" - integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== + version "2.3.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887" + integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw== dependencies: has "^1.0.3" @@ -2528,9 +2545,9 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: kind-of "^6.0.2" is-docker@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.0.tgz#b037c8815281edaad6c2562648a5f5f18839d5f7" - integrity sha512-K4GwB4i/HzhAzwP/XSlspzRdFTI9N8OxJOyOU7Y5Rz+p+WBokXWVWblaJeBkggthmoSV0OoGTH5thJNvplpkvQ== + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -3117,9 +3134,9 @@ jsbn@~0.1.0: integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= jsdom@^16.4.0: - version "16.5.2" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.2.tgz#583fac89a0aea31dbf6237e7e4bedccd9beab472" - integrity sha512-JxNtPt9C1ut85boCbJmffaQ06NBnzkQY/MWO3YxPW8IWS38A26z+B1oBvA9LwKrytewdfymnhi4UNH3/RAgZrg== + version "16.5.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-16.5.3.tgz#13a755b3950eb938b4482c407238ddf16f0d2136" + integrity sha512-Qj1H+PEvUsOtdPJ056ewXM4UJPCi4hhLA8wpiz9F2YvsRBhuFsXxtrIFAgGBDynQA9isAMGE91PfUYbdMPXuTA== dependencies: abab "^2.0.5" acorn "^8.1.0" @@ -3443,12 +3460,12 @@ micromatch@^3.1.4: to-regex "^3.0.2" micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" mime-db@1.47.0: version "1.47.0" @@ -3688,7 +3705,7 @@ node-pre-gyp@^0.11.0: semver "^5.3.0" tar "^4" -node-releases@^1.1.70: +node-releases@^1.1.71: version "1.1.71" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== @@ -3771,9 +3788,9 @@ normalize-url@^4.1.0: integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== npm-bundled@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" - integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== dependencies: npm-normalize-package-bin "^1.0.1" @@ -3830,7 +3847,7 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.0: +object-assign@^4, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -4018,10 +4035,10 @@ pg-connection-string@2.4.0: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.4.0.tgz#c979922eb47832999a204da5dbe1ebf2341b6a10" integrity sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ== -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== pirates@^4.0.1: version "4.0.1" @@ -5049,9 +5066,9 @@ tr46@^2.0.2: punycode "^2.1.1" ts-jest@^26.5.4: - version "26.5.4" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.4.tgz#207f4c114812a9c6d5746dd4d1cdf899eafc9686" - integrity sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg== + version "26.5.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.5.tgz#e40481b6ee4dd162626ba481a2be05fa57160ea5" + integrity sha512-7tP4m+silwt1NHqzNRAPjW1BswnAhopTdc2K3HEkRZjF0ZG2F/e/ypVH0xiZIMfItFtD3CX0XFbwPzp9fIEUVg== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -5257,7 +5274,7 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= @@ -5386,9 +5403,9 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.4: - version "7.4.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== + version "7.4.5" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" + integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== xdg-basedir@^4.0.0: version "4.0.0"