This commit is contained in:
Kilian Hofmann 2021-08-02 15:36:12 +02:00
commit 7204cdab6f
52 changed files with 9105 additions and 0 deletions

8
.babelrc Normal file
View File

@ -0,0 +1,8 @@
{
"presets": ["@babel/env", "@babel/preset-react", "@babel/preset-typescript"],
"plugins": [
["@babel/plugin-transform-typescript", {
"allowNamespaces": true
}]
]
}

4
.browserslistrc Normal file
View File

@ -0,0 +1,4 @@
> 0.5%
last 3 versions
Firefox ESR
not dead

4
.eslintignore Normal file
View File

@ -0,0 +1,4 @@
node_modules
.parcel-cache
dist
build

27
.eslintrc.js Normal file
View File

@ -0,0 +1,27 @@
module.exports = {
settings: {
'import/resolver': {
parcel: {
extensions: ['.ts', '.tsx'], // whatever extra extensions you want to look for
},
},
react: {
version: 'detect',
},
linkComponents: [{ name: 'Link', linkAttribute: 'to' }],
},
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/errors',
'plugin:import/typescript',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
],
rules: {
'import/no-unresolved': [2, { ignore: ['url:', 'svg:'] }],
},
};

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
node_modules
# testing
coverage
# production
dist
build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# AppSync
AppSync.js
.happypack
*.log
webpack/webpack-assets.json
/.parcel-cache
/.netrc
node_modules

6
.parcelrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "@parcel/config-default",
"transformers": {
"svg:*": ["@parcel/transformer-svgo", "@parcel/transformer-raw"]
}
}

8
.postcssrc Normal file
View File

@ -0,0 +1,8 @@
{
"modules": true,
"plugins": {
"autoprefixer": {
"grid": true
}
}
}

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"printWidth": 120,
"tabWidth": 2,
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"arrowParens": "always"
}

20
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"search.exclude": {
"**/node_modules": true,
"yarn.lock": true,
"package.json": true,
"tsconfig.json": true,
".*": true,
},
"typescript.tsdk": "node_modules/typescript/lib",
}

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# Requirements
- Node 14
# Setup
- Install Node 14 (recommend using nvm)
- Install yarn globaly `npm i -g yarn`
- Clone repository `git clone https://git.hofmannnet.myhome-server.de/GermanAirlines/ga-test.git`
- CD into repository
- Fetch dependencies: `yarn install`
# Scripts
- Dev server: `yarn start`
- Prod build: `yarn build`
- Demo prod build: `yarn demo`. Requires Python 3

62
package.json Normal file
View File

@ -0,0 +1,62 @@
{
"name": "ga-test",
"version": "1.0.0",
"description": "GA Build System Test",
"dependencies": {
"@hookform/resolvers": "^2.5.1",
"@reach/router": "^1.3.4",
"bem-classnames": "^1.0.7",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^7.6.10",
"uuid": "^8.3.2",
"yup": "^0.32.9"
},
"devDependencies": {
"@babel/cli": "^7.14.3",
"@babel/core": "^7.14.3",
"@babel/plugin-transform-typescript": "^7.14.3",
"@babel/preset-env": "^7.14.2",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@parcel/config-default": "^2.0.0-alpha.3.1",
"@parcel/core": "^2.0.0-beta.3.1",
"@parcel/transformer-raw": "^2.0.0-alpha.3.1",
"@parcel/transformer-sass": "^2.0.0-beta.3.1",
"@parcel/transformer-svgo": "^2.0.0-beta.3.1",
"@types/jest": "^26.0.23",
"@types/node": "^15.3.1",
"@types/prop-types": "^15.7.3",
"@types/reach__router": "^1.3.7",
"@types/react": "^17.0.6",
"@types/react-dom": "^17.0.5",
"@typescript-eslint/eslint-plugin": "^4.24.0",
"@typescript-eslint/parser": "^4.24.0",
"autoprefixer": "^10.2.5",
"eslint": "^7.26.0",
"eslint-import-resolver-parcel": "^1.10.6",
"eslint-plugin-import": "^2.23.2",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"parcel": "^2.0.0-beta.3.1",
"postcss": "^8.2.15",
"postcss-modules": "^4.0.0",
"prettier": "^2.3.0",
"prettier-plugin-organize-imports": "^2.1.0",
"rimraf": "^3.0.2",
"svgo": "^2.3.0",
"typescript": "^4.2.4"
},
"scripts": {
"build": "rimraf ./build && parcel build src/index.html --dist-dir build --public-url 'plugins/germanairlinesva/react/assets' --log-level verbose",
"clean": "rimraf ./build ./dist ./.parcel-cache",
"format": "prettier --write 'src/**/*.{ts,tsx,scss}'",
"lint": "eslint ./src --fix",
"start": "parcel serve src/index.html --open --log-level verbose",
"demo": "cd ./build && python3 -m http.server 1234"
},
"keywords": [],
"author": "Kilian Kurt Hofmann",
"license": "MIT"
}

54
src/App.tsx Normal file
View File

@ -0,0 +1,54 @@
import { Redirect, Router } from '@reach/router';
import React from 'react';
import classNames from './assets/scss/App.scss';
import { useSideBar } from './context/SideBar/SideBarContext';
import MenuBar from './layout/MenuBar/MenuBar';
import SideBar from './layout/SideBar/SideBar';
import HomePage from './pages/HomePage/HomePage';
import NotFoundPage from './pages/NotFound/NotFoundPage';
import LogbookPage from './pages/user/LogbookPage/LogbookPage';
import ProfilePage from './pages/user/ProfilePage/ProfilePage';
import ProfilePages from './pages/user/ProfilePages';
import UserPages from './pages/user/UserPages';
import routes from './routes';
import OnRouteChange from './util/OnRouteChange';
import resolveClassName from './util/resolveClassName';
const App = (): React.ReactElement => {
const { state, toggleState } = useSideBar();
const classes = {
name: 'container',
modifiers: ['sidebarExtended'],
};
return (
<>
<MenuBar />
<div className={resolveClassName(classNames, classes, { sidebarExtended: state == 'open' })}>
<SideBar />
<Router
className={classNames.router}
onClick={() => {
if (state == 'open') toggleState();
}}
>
<HomePage path={routes.company.home} />
<UserPages path={routes.personal.base}>
<Redirect from="/" to="/" noThrow />
<LogbookPage path={routes.personal.logbook} />
<ProfilePages path={routes.personal.profile.base}>
<ProfilePage path="/" />
<ProfilePage path={routes.personal.profile.others} />
</ProfilePages>
</UserPages>
<NotFoundPage default />
</Router>
<OnRouteChange />
</div>
</>
);
};
export default App;

35
src/assets/img/arrow.svg Normal file
View File

@ -0,0 +1,35 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 284.935 284.936" style="enable-background:new 0 0 284.935 284.936;" xml:space="preserve">
<g>
<path d="M222.701,135.9L89.652,2.857C87.748,0.955,85.557,0,83.084,0c-2.474,0-4.664,0.955-6.567,2.857L62.244,17.133 c-1.906,1.903-2.855,4.089-2.855,6.567c0,2.478,0.949,4.664,2.855,6.567l112.204,112.204L62.244,254.677 c-1.906,1.903-2.855,4.093-2.855,6.564c0,2.477,0.949,4.667,2.855,6.57l14.274,14.271c1.903,1.905,4.093,2.854,6.567,2.854 c2.473,0,4.663-0.951,6.567-2.854l133.042-133.044c1.902-1.902,2.854-4.093,2.854-6.567S224.603,137.807,222.701,135.9z" fill="#999999"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 883 B

41
src/assets/img/pilot.svg Normal file
View File

@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 495.992 495.992" style="enable-background:new 0 0 495.992 495.992;" xml:space="preserve" width="512px" height="512px">
<g>
<g>
<g>
<path d="M447.816,390.184c-5.376-32.232-27.976-58.68-58.976-69.016L320,298.224v-10.232h-16v14.976l-12.344,47.312 l-31.904-38.288h-23.504l-31.904,38.288L192,302.968v-14.976h-16v10.232l-68.832,22.944c-31,10.336-53.6,36.784-58.976,69.016 l-17.64,105.808H465.44L447.816,390.184z M120.84,479.992l7.12-71.2l-15.92-1.592l-7.28,72.8H49.448l14.52-87.184 c4.4-26.368,22.888-48.008,48.256-56.472l33.408-11.136l-19.608,45.752l23.664,15.776l-15.104,30.184l49.056,63.072H120.84z M203.912,479.992l-50.496-64.928l16.896-33.816l-24.336-16.224l19.96-46.576l12.48-4.16l43.232,165.704H203.912z M220.384,411.768l-11.072-42.448l4.792-5.752l9.656,14.48L220.384,411.768z M263.984,456.328l-6.168,23.664h-19.64L232,456.328 l8.232-82.392l-15.456-23.184l18.976-22.76h8.496l18.96,22.76l-15.456,23.184L263.984,456.328z M281.896,363.568l4.792,5.752 l-11.072,42.448l-3.376-33.72L281.896,363.568z M274.352,479.992l43.232-165.704l12.48,4.16l19.96,46.576l-24.336,16.224 l16.896,33.816l-50.496,64.928H274.352z M391.24,479.992l-7.28-72.8l-15.92,1.592l7.12,71.2h-62.8l49.056-63.072l-15.104-30.184 l23.664-15.776L350.368,325.2l33.408,11.136c25.368,8.464,43.856,30.104,48.256,56.472l14.52,87.184H391.24z" fill="#FFFFFF"/>
<path d="M128,122.888v21.304c-9.656,7.312-16,18.784-16,31.792c0,20.528,15.6,37.296,35.536,39.552 c4.08,14.04,11.616,26.912,22.24,37.536l31.192,31.192c7.552,7.552,17.6,11.72,28.288,11.72h37.488 c10.688,0,20.736-4.168,28.288-11.72l31.192-31.192c10.624-10.624,18.152-23.504,22.24-37.536 C368.4,213.288,384,196.52,384,175.992c0-13.016-6.344-24.488-16-31.792v-21.304C398.896,109.64,416,91.728,416,72 c0-40.376-73.8-72-168-72S80,31.624,80,72C80,91.72,97.104,109.632,128,122.888z M128,175.992c0-10.416,6.712-19.216,16-22.528 v37.392c0,2.616,0.152,5.208,0.376,7.784C134.896,195.432,128,186.544,128,175.992z M336,190.856 c0,19.232-7.496,37.312-21.088,50.912L283.72,272.96c-4.536,4.536-10.56,7.032-16.976,7.032h-37.488 c-6.416,0-12.448-2.496-16.976-7.032l-31.192-31.192c-13.592-13.6-21.088-31.68-21.088-50.912v-44.944 c19.632-4.464,51.112-9.92,88-9.92c36.816,0,68.336,5.464,88,9.928V190.856z M351.624,198.632 c0.224-2.568,0.376-5.16,0.376-7.776v-37.392c9.288,3.312,16,12.112,16,22.528C368,186.544,361.104,195.432,351.624,198.632z M248,15.992c89.576,0,152,29.512,152,56c0,11.832-11.88,23.912-32.952,33.912l-4.528-1.504 c-0.928-0.304-23.088-7.592-57.424-12.376l-2.2,15.84c23.128,3.224,40.696,7.696,49.104,10.08v15.376 c-17.776-4.76-56.368-13.328-104-13.328c-47.632,0-86.224,8.568-104,13.336v-15.384c8.384-2.376,25.896-6.84,49.104-10.072 l-2.2-15.84c-34.344,4.784-56.496,12.072-57.424,12.376l-4.528,1.504C107.88,95.904,96,83.824,96,71.992 C96,45.504,158.424,15.992,248,15.992z" fill="#FFFFFF"/>
<path d="M211.392,87.992c6.192,14.104,20.248,24,36.608,24s30.416-9.896,36.608-24H308l36-48h-56v16h24l-12,16h-12 c0-22.056-17.944-40-40-40c-22.056,0-40,17.944-40,40h-12l-12-16h24v-16h-56l36,48H211.392z M248,47.992 c13.232,0,24,10.768,24,24s-10.768,24-24,24s-24-10.768-24-24S234.768,47.992,248,47.992z" fill="#FFFFFF"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

22
src/assets/scss/App.scss Normal file
View File

@ -0,0 +1,22 @@
@import './variables';
.container {
display: flex;
@media (max-width: $mediumScreen) {
& {
transform: none;
transition: transform 150ms ease-in-out;
}
&--sidebarExtended {
transform: translateX($sidebarWidth);
transition: transform 150ms ease-in-out;
}
}
}
.router {
flex-grow: 1;
padding: 0.625rem;
}

View File

@ -0,0 +1,21 @@
// COLORS
$colDarkBlue: #000b29;
$colDarkBlue-10: #1a2543;
$colDarkBlue-20: #333e5c;
$colDarkBlue-30: #4d5876;
$colLightBlue: #0070b9;
$colLightBlue-10: #1a8ad3;
$colLightBlue-20: #33a3ec;
$colLightBlue-30: #4dbdff;
$colGray: #999999;
$colGray-10: #b3b3b3;
$colGray-20: #cccccc;
$colGray-30: #e6e6e6;
// SIZES
$mediumScreen: 768px;
$menubarHeight: 3.5rem;
$sidebarWidth: 250px;

View File

@ -0,0 +1,67 @@
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
interface ICollapseGroupContext {
/** Register a new collapse to the group */
register: ((id: string, callback: () => void) => void) | null;
/** Unregister the collapse with the given ID from the group */
unregister: ((id: string) => void) | null;
/** Collapse all registered collapses but the one with the given ID */
collapse: ((id: string) => void) | null;
}
export const CollapseGroupContext = React.createContext<ICollapseGroupContext>({
register: null,
unregister: null,
collapse: null,
});
export const useCollapseGroup = (): ICollapseGroupContext => {
const { register, unregister, collapse } = React.useContext(CollapseGroupContext);
if (register !== null && unregister !== null && collapse !== null) {
return {
register,
unregister,
collapse,
};
} else {
throw new Error("Couldn't find context. Is your component inside a CollapseGroupProvider?");
}
};
export const CollapseGroupProvider: React.FunctionComponent = ({ children }: { children: React.ReactNode }) => {
const [ID] = React.useState(uuidv4());
const collapses = React.useRef<{ id: string; callback: () => void }[]>([]);
const register = (id: string, callback: () => void) => {
console.log('Group', ID, 'registered collapse', id);
collapses.current = [...collapses.current, { id, callback }];
};
const unregister = (id: string) => {
console.log('Group', ID, 'unregistered collapse', id);
collapses.current = collapses.current.map((_collapse) => {
if (_collapse.id != id) return _collapse;
});
};
const collapse = (id: string) => {
console.log('Group', ID, 'collapse all but', id);
collapses.current.forEach((_collapse) => _collapse.id != id && _collapse.callback());
};
return (
<CollapseGroupContext.Provider
value={{
register,
unregister,
collapse,
}}
>
{children}
</CollapseGroupContext.Provider>
);
};
export default CollapseGroupProvider;

View File

@ -0,0 +1,47 @@
import React from 'react';
interface ISideBarContext {
/** State of side bar */
state: 'open' | 'closed' | null;
/** Toggle side bar state */
toggleState: (() => void) | null;
}
export const SideBarContext = React.createContext<ISideBarContext>({
state: null,
toggleState: null,
});
export const useSideBar = (): ISideBarContext => {
const { state, toggleState } = React.useContext(SideBarContext);
if (state !== null && toggleState !== null) {
return {
state,
toggleState,
};
} else {
throw new Error("Couldn't find context. Is your component inside a SidebarProvider?");
}
};
export const SideBarProvider: React.FunctionComponent = ({ children }: { children: React.ReactNode }) => {
const [state, setState] = React.useState<'open' | 'closed'>('closed');
const toggleState = () => {
setState(state == 'open' ? 'closed' : 'open');
};
return (
<SideBarContext.Provider
value={{
state,
toggleState,
}}
>
{children}
</SideBarContext.Provider>
);
};
export default SideBarProvider;

4
src/definition/scss.d.ts vendored Normal file
View File

@ -0,0 +1,4 @@
declare module '*.scss' {
const classNames: { [className: string]: string };
export = classNames;
}

9
src/definition/transformers.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module 'url:*' {
const asset: string;
export = asset;
}
declare module 'svg:*.svg' {
const asset: string;
export = asset;
}

33
src/index.html Normal file
View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>GA Test</title>
<style>
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
}
</style>
</head>
<body>
<noscript>You need to enable Javascript to run this application.</noscript>
<div id="root">
<!-- Your react app will be rendered here -->
</div>
<script src="../src/index.tsx"></script>
</body>
</html>

11
src/index.tsx Normal file
View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import SideBarProvider from './context/SideBar/SideBarContext';
ReactDOM.render(
<SideBarProvider>
<App />
</SideBarProvider>,
document.getElementById('root')
);

View File

@ -0,0 +1,23 @@
@import '../../assets/scss/variables';
.navbar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 0.5rem 1rem;
height: $menubarHeight;
background-color: $colDarkBlue;
}
.navbar--fixed-top {
position: fixed;
top: 0;
right: 0;
left: 0;
z-index: 1030;
}
.spacer {
height: $menubarHeight;
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import { useSideBar } from '../../context/SideBar/SideBarContext';
import Button from '../../lib/Button/Button';
import useWindowDimensions from '../../util/useWindowDimensions';
import classNames from './MenuBar.scss';
const MenuBar = (): React.ReactElement => {
const { width } = useWindowDimensions();
const { state, toggleState } = useSideBar();
React.useEffect(() => {
if (width > 768 && state == 'open') toggleState();
}, [width]); //eslint-disable-line react-hooks/exhaustive-deps
return (
<>
<nav className={`${classNames.navbar} ${classNames['navbar--fixed-top']}`}>
{width <= 768 && <Button color="navbar" onClick={toggleState} />}
</nav>
<div className={classNames.spacer}></div>
</>
);
};
export default MenuBar;

View File

@ -0,0 +1,79 @@
@import '../../../assets/scss/variables';
.menu-item {
line-height: 2.5rem;
display: block;
color: $colGray;
text-decoration: none;
.name {
text-indent: 1.25rem;
height: 4.063rem;
font-size: 1.25rem;
font-weight: bold;
line-height: 3.75rem;
&--active {
color: white;
background-color: white;
background-color: rgba(255, 255, 255, 0.4);
}
}
&--subMenu {
.icon {
width: 2.5rem;
height: 2.5rem;
padding: 0.9375rem;
transform: rotate(0deg);
transition: 0.35s transform ease;
float: right;
&--open {
transform: rotate(90deg);
}
}
.name {
cursor: pointer;
}
}
&:hover,
&--subMenu .name:hover {
color: white;
background-color: white;
background-color: rgba(255, 255, 255, 0.2);
}
&--noHover:hover {
color: $colGray;
background-color: transparent;
}
&--active {
color: white;
background-color: white;
background-color: rgba(255, 255, 255, 0.4);
}
& {
text-indent: 1.875rem;
}
& & {
text-indent: 2.5rem;
.name {
text-indent: 2.5rem;
height: auto;
line-height: 2.5rem;
font-weight: normal;
font-size: 1rem;
}
}
& & & {
text-indent: 3.125rem;
}
}

View File

@ -0,0 +1,110 @@
import { Link, Location } from '@reach/router';
import React from 'react';
import arrow from 'svg:../../../assets/img/arrow.svg';
import { v4 as uuidv4 } from 'uuid';
import { useCollapseGroup } from '../../../context/CollapseGroup/CollapseGroupContext';
import Collapse from '../../../lib/Collapse/Collapse';
import resolveClassName from '../../../util/resolveClassName';
import classNames from './MenuItem.scss';
interface IMenuItemProps {
/** Name of menu entry */
name: React.ReactNode;
/** Null for static, string to link, array of strings for submenu */
to?: string | string[];
/** True if matching for active shall match even with url parameters */
hasParameters?: boolean;
}
const MenuItem: React.FunctionComponent<IMenuItemProps> = ({
name,
to,
hasParameters,
children,
}: React.PropsWithChildren<IMenuItemProps>) => {
const classesMenuItem = {
name: 'menu-item',
modifiers: ['active', 'noHover', 'subMenu'],
};
const classesMenuName = {
name: 'name',
modifiers: ['active'],
};
const classesMenuIcon = {
name: 'icon',
modifiers: ['open'],
};
const [subMenuOpen, setSubMenuOpen] = React.useState(false);
const [ID] = React.useState(uuidv4());
// Conditional Hook calls are illegal
// Since only an array for to creates a collapsible submenu, this is safe(ish)
if (Array.isArray(to)) {
try {
//eslint-disable-next-line react-hooks/rules-of-hooks, no-var
var { register, unregister, collapse } = useCollapseGroup();
} catch {} //eslint-disable-line no-empty
}
React.useEffect(() => {
if (register) {
register(ID, () => {
setSubMenuOpen(false);
});
return () => unregister(ID);
}
}, []); //eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
if (collapse) {
if (subMenuOpen && collapse) collapse(ID);
}
}, [subMenuOpen]); //eslint-disable-line react-hooks/exhaustive-deps
return to ? (
Array.isArray(to) ? (
<Location>
{({ location }) => (
<div className={resolveClassName(classNames, classesMenuItem, { noHover: true, subMenu: true })}>
<div
className={resolveClassName(classNames, classesMenuName, {
active:
to.findIndex((_to) => (hasParameters ? location.pathname.includes(_to) : _to == location.pathname)) !=
-1,
})}
onClick={() => setSubMenuOpen(!subMenuOpen)}
>
{name}
<img className={resolveClassName(classNames, classesMenuIcon, { open: subMenuOpen })} src={arrow} />
</div>
<Collapse id={ID} open={subMenuOpen}>
{children}
</Collapse>
</div>
)}
</Location>
) : (
<Location>
{({ location }) => (
<Link
to={to}
className={resolveClassName(classNames, classesMenuItem, {
active: hasParameters ? location.pathname.includes(to) : to == location.pathname,
})}
>
{name}
</Link>
)}
</Location>
)
) : (
<div className={resolveClassName(classNames, classesMenuItem, { noHover: true })}>
<div className={classNames.name}> {name}</div>
{children}
</div>
);
};
export default MenuItem;

View File

@ -0,0 +1,33 @@
@import '../../assets/scss/variables';
.sidebar {
background-color: $colDarkBlue;
width: $sidebarWidth;
min-height: calc(100vh - #{$menubarHeight});
margin-left: -$sidebarWidth;
flex-shrink: 0;
.memberData {
width: 100%;
padding-top: 1.25rem;
text-align: center !important;
span {
color: white;
}
h4 {
color: white;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
font-size: 1.5rem;
margin-top: 0;
}
}
}
@media (min-width: $mediumScreen) {
.sidebar {
margin-left: 0;
}
}

View File

@ -0,0 +1,110 @@
import React from 'react';
import CollapseGroupProvider from '../../context/CollapseGroup/CollapseGroupContext';
import MemberImage from '../../lib/MemberImage/MemberImage';
import Pill from '../../lib/Pill/Pill';
import routes from '../../routes';
import MenuItem from './MenuItem/MenuItem';
import classNames from './SideBar.scss';
const SideBar = (): React.ReactElement => {
return (
<div className={classNames.sidebar}>
<div className={classNames.memberData}>
<MemberImage />
<h4>VGA1582</h4>
<span>Kilian Hofmann</span>
</div>
<CollapseGroupProvider>
<MenuItem
name={
<>
Allgemein<Pill>4</Pill>
</>
}
>
<MenuItem
name="Unternehmen"
to={[routes.company.home, routes.company.travel, routes.company.cargo, routes.company.civil]}
>
<MenuItem to={routes.company.home} name="Übersicht" />
<MenuItem to={routes.company.travel} name="GA - Travel" />
<MenuItem to={routes.company.cargo} name="GA - Cargo" />
<MenuItem to={routes.company.civil} name="GA - Civil" />
</MenuItem>
<MenuItem
name="Community"
to={[
`${routes.community.base}${routes.community.magazine}`,
`${routes.community.base}${routes.community.downloads}`,
`${routes.community.base}${routes.community.ranking}`,
`${routes.community.base}${routes.community.map}`,
`${routes.community.base}${routes.community.news}`,
`${routes.community.base}${routes.community.forum.base}`,
`${routes.community.base}${routes.community.events}`,
]}
hasParameters
>
<MenuItem to={`${routes.community.base}${routes.community.magazine}`} name="Magazin" />
<MenuItem to={`${routes.community.base}${routes.community.downloads}`} name="Downloads" />
<MenuItem to={`${routes.community.base}${routes.community.ranking}`} name="Rangliste" />
<MenuItem to={`${routes.community.base}${routes.community.map}`} name="Weltkarte" />
<MenuItem to={`${routes.community.base}${routes.community.news}`} name="Nachrichten" />
<MenuItem to={`${routes.community.base}${routes.community.forum.base}`} name="Forum" hasParameters />
<MenuItem to={`${routes.community.base}${routes.community.events}`} name="Events" />
</MenuItem>
</MenuItem>
<MenuItem name="Flugbetrieb">
<MenuItem to={`${routes.booking.base}${routes.booking.planner}`} name="Umlaufplanner" />
<MenuItem
name="Buchen"
to={[
`${routes.booking.base}${routes.booking.passenger}`,
`${routes.booking.base}${routes.booking.charter}`,
`${routes.booking.base}${routes.booking.cargo}`,
`${routes.booking.base}${routes.booking.heritage}`,
`${routes.booking.base}${routes.booking.visual}`,
]}
>
<MenuItem to={`${routes.booking.base}${routes.booking.passenger}`} name="Passenger" />
<MenuItem to={`${routes.booking.base}${routes.booking.charter}`} name="Charter" />
<MenuItem to={`${routes.booking.base}${routes.booking.cargo}`} name="Cargo" />
<MenuItem to={`${routes.booking.base}${routes.booking.heritage}`} name="Heritage" />
<MenuItem to={`${routes.booking.base}${routes.booking.visual}`} name="Visual" />
</MenuItem>
<MenuItem
name="Briefing"
to={[
`${routes.briefing.base}${routes.briefing.passenger}`,
`${routes.briefing.base}${routes.briefing.charter}`,
`${routes.briefing.base}${routes.briefing.cargo}`,
`${routes.briefing.base}${routes.briefing.heritage}`,
`${routes.briefing.base}${routes.briefing.visual}`,
]}
>
<MenuItem to={`${routes.briefing.base}${routes.briefing.passenger}`} name="Passenger" />
<MenuItem to={`${routes.briefing.base}${routes.briefing.charter}`} name="Charter" />
<MenuItem to={`${routes.briefing.base}${routes.briefing.cargo}`} name="Cargo" />
<MenuItem to={`${routes.briefing.base}${routes.briefing.heritage}`} name="Heritage" />
<MenuItem to={`${routes.briefing.base}${routes.briefing.visual}`} name="Visual" />
</MenuItem>
</MenuItem>
<MenuItem name="Flugschule">
<MenuItem to={routes.school.base} name="Übersicht" />
<MenuItem to={[`${routes.school.base}${routes.school.documents}`]} name="Ausbildung">
<MenuItem to={`${routes.school.base}${routes.school.documents}`} name="Unterlagen" />
</MenuItem>
<MenuItem to={`${routes.school.base}${routes.school.exam.rank}`} name="Rangprüfung" />
<MenuItem to={`${routes.school.base}${routes.school.exam.typerating}`} name="Typerating" />
</MenuItem>
<MenuItem name="Persönliches">
<MenuItem to={`${routes.personal.base}${routes.personal.logbook}`} name="Logbuch" />
<MenuItem to={`${routes.personal.base}${routes.personal.profile.base}`} name="Profil" />
</MenuItem>
</CollapseGroupProvider>
<div style={{ backgroundColor: 'red' }}>END OF BAR</div>
</div>
);
};
export default SideBar;

View File

@ -0,0 +1,93 @@
@import '../../assets/scss/variables';
.button {
&:not(:disabled),
[type='button']:not(:disabled),
[type='reset']:not(:disabled),
[type='submit']:not(:disabled) {
cursor: pointer;
}
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;
background-color: $colDarkBlue;
border-color: $colDarkBlue;
color: white;
margin: 0.3125rem;
&:hover {
background-color: white;
border-color: $colDarkBlue;
color: $colDarkBlue;
}
&:active {
background-color: #0062cc;
border-color: #005cbf;
color: white;
}
&:disabled {
opacity: 0.65;
}
&--danger {
background-color: #dc3545;
border-color: #dc3545;
&:hover {
background-color: #c82333;
border-color: #bd2130;
color: white;
}
&:active {
background-color: #bd2130;
border-color: #b21f2d;
}
&:disabled {
opacity: 0.65;
}
}
&--navbar {
margin: 0;
padding: 0.25rem 0.75rem;
font-size: 1.25rem;
line-height: 1;
background-color: transparent;
border: 1px solid transparent;
border-radius: 0.25rem;
color: rgba(255, 255, 255, 0.5);
border-color: rgba(255, 255, 255, 0.1);
&:hover,
&:active {
background-color: transparent;
border: 1px solid transparent;
border-color: rgba(255, 255, 255, 0.1);
}
.toggler {
display: inline-block;
width: 1.5em;
height: 1.5em;
vertical-align: middle;
content: '';
background: no-repeat center center;
background-size: 100% 100%;
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3e%3cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
}
}

24
src/lib/Button/Button.tsx Normal file
View File

@ -0,0 +1,24 @@
import React from 'react';
import resolveClassName from '../../util/resolveClassName';
import classNames from './Button.scss';
interface IButtonProps
extends React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement> {
/** Button color */
color?: 'danger' | 'navbar';
}
const Button: React.FunctionComponent<IButtonProps> = ({ color, children, ...props }: IButtonProps) => {
const classes = {
name: 'button',
modifiers: ['color'],
};
return (
<button {...props} className={resolveClassName(classNames, classes, { color: color })}>
{color == 'navbar' ? <span className={classNames.toggler}></span> : children}
</button>
);
};
export default Button;

View File

@ -0,0 +1,5 @@
.collapse {
overflow: hidden;
height: 0;
transition: height 0.35s ease;
}

View File

@ -0,0 +1,35 @@
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import classNames from './Collapse.scss';
interface ICollapseProps {
/** True if collapse is open */
open: boolean;
/** Optional ID for use with collapse groups, if null internal ID will be generated */
id?: string;
}
const Collapse: React.FunctionComponent<React.PropsWithChildren<ICollapseProps>> = ({
open,
id,
children,
}: React.PropsWithChildren<ICollapseProps>) => {
const [ID] = React.useState(uuidv4());
const [height, setHeight] = React.useState(0);
React.useLayoutEffect(() => {
setHeight(
Array.from(document.getElementById(id ?? ID).children)
.map((child) => child.clientHeight)
.reduce((acc, val) => (acc += val))
);
}, []); //eslint-disable-line react-hooks/exhaustive-deps
return (
<div id={id ?? ID} className={classNames.collapse} style={{ height: open ? height : 0 }}>
{children}
</div>
);
};
export default Collapse;

View File

@ -0,0 +1,14 @@
.image {
width: 6.25rem;
height: 6.25rem;
margin: auto;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
border: 2px solid white;
border-radius: 6.25rem;
&--none {
background-size: 4.5rem;
}
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import pilot from 'svg:../../assets/img/pilot.svg';
import resolveClassName from '../../util/resolveClassName';
import classNames from './MemberImage.scss';
const MemberImage = ({ image }: { image?: string }): React.ReactElement => {
const classes = {
name: 'image',
modifiers: ['none'],
};
return (
<div
className={resolveClassName(classNames, classes, { none: image == null })}
style={{ backgroundImage: `url('${image ?? pilot}')` }}
/>
);
};
export default MemberImage;

16
src/lib/Pill/Pill.scss Normal file
View File

@ -0,0 +1,16 @@
@import '../../assets/scss/variables';
.pill {
padding: 0.25em 0.6em 0.25em 0.6em;
font-size: 75%;
font-weight: 700;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 10rem;
color: $colDarkBlue;
background-color: white;
display: inline;
margin: 0.5rem;
}

8
src/lib/Pill/Pill.tsx Normal file
View File

@ -0,0 +1,8 @@
import React from 'react';
import classNames from './Pill.scss';
const Pill: React.FunctionComponent = ({ children }: { children: React.ReactNode }) => {
return <div className={classNames.pill}>{children}</div>;
};
export default Pill;

View File

@ -0,0 +1,120 @@
@import '../../assets/scss/variables';
.split-flap {
margin: 1vw;
width: 40px;
height: 40px;
line-height: 40px;
font-size: 40px;
text-align: center;
font-family: monospace;
perspective: 100px;
color: white;
.top-next {
height: 50%;
width: 100%;
background-color: $colLightBlue;
overflow: hidden;
z-index: 0;
position: relative;
border-radius: 5px 5px 0 0;
}
.top-current {
margin-top: -50%;
height: 50%;
width: 100%;
background-color: $colLightBlue;
overflow: hidden;
z-index: 1;
position: relative;
border-radius: 5px 5px 0 0;
transform-origin: bottom;
transform: rotateX(0deg);
&--animate {
animation: flipUp ease-in 1;
animation-duration: 2s;
animation-fill-mode: forwards;
}
&--stop {
animation: none;
}
}
.bottom-current {
height: 50%;
width: 100%;
background-image: linear-gradient(180deg, $colLightBlue, $colDarkBlue);
overflow: hidden;
z-index: 1;
position: relative;
border-radius: 0 0 5px 5px;
span {
transform: translateY(-50%);
display: block;
}
}
.bottom-next {
margin-top: -50%;
height: 50%;
width: 100%;
background-image: linear-gradient(180deg, $colLightBlue, $colDarkBlue);
overflow: hidden;
z-index: 1;
position: relative;
border-radius: 0 0 5px 5px;
transform-origin: top;
transform: rotateX(90deg);
span {
transform: translateY(-50%);
display: block;
}
&--animate {
animation: flipDown ease-out 1;
animation-duration: 2s;
animation-fill-mode: forwards;
}
&--stop {
animation: none;
}
}
@keyframes flipDown {
0% {
transform: rotateX(90deg);
}
50% {
transform: rotateX(90deg);
}
100% {
transform: rotateX(0deg);
}
}
@keyframes flipUp {
0% {
transform: rotateX(0deg);
}
50% {
transform: rotateX(-90deg);
}
100% {
transform: rotateX(-90deg);
}
}
}
.container {
display: flex;
justify-content: center;
background-color: #111111;
border: solid 5px black;
}

View File

@ -0,0 +1,177 @@
import React from 'react';
import resolveClassName from '../../util/resolveClassName';
import classNames from './SplitFlapBoard.scss';
interface ISplitFlapBoardProps {
/** Message the split flap board shall display */
message: string;
/** True to start the animation */
start: boolean;
/** Size of one split flap module, default of 40px */
size?: number;
/** Duration of one transition animation */
duration?: number;
}
const SplitFlapBoard: React.FunctionComponent<ISplitFlapBoardProps> = ({
message,
start,
size,
duration,
}: ISplitFlapBoardProps) => {
const classesMain = {
name: 'split-flap',
modifiers: [],
};
const classesTopNext = {
name: 'top-next',
modifiers: [],
};
const classesTopCurrent = {
name: 'top-current',
modifiers: ['stop', 'animate'],
};
const classesBottomCurrent = {
name: 'bottom-current',
modifiers: [],
};
const classesBottomNext = {
name: 'bottom-next',
modifiers: ['stop', 'animate'],
};
const typeSet = [
' ',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'0',
];
const [animate, setAnimate] = React.useState(false);
const currentCharacter = React.useRef<number[]>(Array(message.length).fill(0));
const nextCharacter = React.useRef<number[]>(Array(message.length).fill(1));
const flapStop = React.useRef<boolean[]>(Array(message.length).fill(false));
const timeout = React.useRef<NodeJS.Timeout>();
const animation = () => {
for (let i = 0; i < currentCharacter.current.length; i++) {
if (typeSet[currentCharacter.current[i]] !== message.split('')[i]) {
currentCharacter.current[i] = nextCharacter.current[i];
nextCharacter.current[i] = (nextCharacter.current[i] % 36) + 1;
if (typeSet[currentCharacter.current[i]] === message.split('')[i]) {
flapStop.current[i] = true;
}
}
}
setAnimate(false);
if (!flapStop.current.reduce((acc, val) => acc && val)) {
timeout.current = setTimeout(() => {
setAnimate(true);
timeout.current = setTimeout(animation, duration ? duration * 1000 : 2000);
}, Math.max(25, Math.random() * 100));
}
};
React.useEffect(() => {
if (start) {
setAnimate(true);
timeout.current = setTimeout(animation, duration ? duration * 1000 : 2000);
}
}, [start]); //eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
return () => clearTimeout(timeout.current);
}, []);
return (
<div className={classNames.container}>
{message.split('').map((char, index) => {
return (
<div
key={`${char}-${index}`}
className={resolveClassName(classNames, classesMain, {})}
style={{
width: size ?? null,
height: size ?? null,
lineHeight: size ? `${size}px` : null,
fontSize: size ?? null,
perspective: size ? size * 10 : null,
}}
>
{/* TOP NEXT CHAR */}
<div
className={resolveClassName(classNames, classesTopNext, {})}
style={{ borderRadius: size ? `${size / 8}px ${size / 8}px 0 0` : null }}
>
{typeSet[nextCharacter.current[index]]}
</div>
{/* TOP CUR CHAR */}
<div
className={resolveClassName(classNames, classesTopCurrent, { animate, stop: flapStop.current[index] })}
style={{
animationDuration: `${duration}s` ?? null,
borderRadius: size ? `${size / 8}px ${size / 8}px 0 0` : null,
}}
>
{typeSet[currentCharacter.current[index]]}
</div>
{/* BOTTOM CUR CHAR */}
<div
className={resolveClassName(classNames, classesBottomCurrent, {})}
style={{ borderRadius: size ? `0 0 ${size / 8}px ${size / 8}px` : null }}
>
<span>{typeSet[currentCharacter.current[index]]}</span>
</div>
{/* BOTTOM NEXT CHAR */}
<div
className={resolveClassName(classNames, classesBottomNext, { animate, stop: flapStop.current[index] })}
style={{
animationDuration: `${duration}s` ?? null,
borderRadius: size ? `0 0 ${size / 8}px ${size / 8}px` : null,
}}
>
<span>{typeSet[nextCharacter.current[index]]}</span>
</div>
</div>
);
})}
</div>
);
};
export default SplitFlapBoard;

View File

@ -0,0 +1,8 @@
import { RouteComponentProps } from '@reach/router';
import React from 'react';
const HomePage: React.FunctionComponent<RouteComponentProps> = () => {
return <>Home</>;
};
export default HomePage;

View File

@ -0,0 +1,21 @@
import { RouteComponentProps, useLocation } from '@reach/router';
import React from 'react';
import SplitFlapBoard from '../../lib/SplitFlapBoard/SplitFlapBoard';
import useWindowDimensions from '../../util/useWindowDimensions';
const NotFoundPage: React.FunctionComponent<RouteComponentProps> = () => {
const { pathname } = useLocation();
const { width } = useWindowDimensions();
const vw1 = (2 * width) / 100;
const size = width > 768 ? (width - 300 - 8 * vw1) / 8 : (width - 50 - 8 * vw1) / 8;
return (
<>
<SplitFlapBoard key={`${pathname}1`} message="SEITE" duration={0.15} size={size} start />
<SplitFlapBoard key={`${pathname}2`} message="NICHT" duration={0.15} size={size} start />
<SplitFlapBoard key={`${pathname}3`} message="GEFUNDEN" duration={0.15} size={size} start />
</>
);
};
export default NotFoundPage;

View File

@ -0,0 +1,8 @@
import { RouteComponentProps } from '@reach/router';
import React from 'react';
const LogbookPage: React.FunctionComponent<RouteComponentProps> = () => {
return <>Logbook</>;
};
export default LogbookPage;

View File

@ -0,0 +1,21 @@
import { RouteComponentProps } from '@reach/router';
import React from 'react';
import useParams from '../../../util/useParams';
interface IProfilePageProps {
/** Null if own profile, otherwise ID of member whose profile shall be displayed */
memberID?: number;
}
const ProfilePage: React.FunctionComponent<RouteComponentProps> = ({
children,
}: React.PropsWithChildren<RouteComponentProps>) => {
const { memberID } = useParams<IProfilePageProps>();
return (
<>
Hey {memberID ?? 'SELF'} {children}
</>
);
};
export default ProfilePage;

View File

@ -0,0 +1,10 @@
import { RouteComponentProps } from '@reach/router';
import React from 'react';
const ProfilePages: React.FunctionComponent<RouteComponentProps> = ({
children,
}: React.PropsWithChildren<RouteComponentProps>) => {
return <>{children}</>;
};
export default ProfilePages;

View File

@ -0,0 +1,10 @@
import { RouteComponentProps } from '@reach/router';
import React from 'react';
const UserPages: React.FunctionComponent<RouteComponentProps> = ({
children,
}: React.PropsWithChildren<RouteComponentProps>) => {
return <>{children}</>;
};
export default UserPages;

63
src/routes.ts Normal file
View File

@ -0,0 +1,63 @@
const routes = {
company: {
home: '/',
travel: '/travel',
cargo: '/cargo',
civil: '/civil',
},
community: {
base: '/community',
magazine: '/magazine',
downloads: '/downloads',
ranking: '/ranking',
map: '/map',
news: '/news',
forum: {
base: '/forum',
board: ':board',
thread: ':thread',
},
events: '/events',
},
booking: {
base: '/booking',
planner: '/planner',
passenger: '/passenger',
charter: '/charter',
cargo: '/cargo',
heritage: '/heritage',
visual: '/visual',
},
briefing: {
base: '/briefing',
passenger: '/passenger',
charter: '/charter',
cargo: '/cargo',
heritage: '/heritage',
visual: '/visual',
},
school: {
base: '/schooling',
documents: '/documents',
exam: {
rank: '/rankExam',
typerating: '/typeratingExam',
},
},
personal: {
base: '/personal',
logbook: '/logbook',
profile: {
base: '/profile',
others: ':memberID',
generate: (id: string): string => `/personal/profile/${id}`,
},
},
};
export default routes;

View File

@ -0,0 +1,24 @@
import { Location, WindowLocation } from '@reach/router';
import React from 'react';
interface IOnRouteChangeWorkerProps {
/** Current router location */
location: WindowLocation;
}
const OnRouteChangeWorker: React.FunctionComponent<IOnRouteChangeWorkerProps> = () => {
React.useLayoutEffect(() => {
if (window.scrollY != 0) {
window.scrollTo(0, 0);
} else {
window.scrollTo(0, 1);
}
}, [location.pathname]); //eslint-disable-line react-hooks/exhaustive-deps
return null;
};
const OnRouteChange = (): React.ReactElement => (
<Location>{({ location }) => <OnRouteChangeWorker location={location} />}</Location>
);
export default OnRouteChange;

View File

@ -0,0 +1,14 @@
import cx from 'bem-classnames';
const resolveClassName = (
classNames: { [name: string]: string },
classes: Record<string, unknown>,
bemState: Record<string, unknown>
): string => {
return cx(classes, bemState)
.split(' ')
.map((cn) => classNames[cn])
.join(' ');
};
export default resolveClassName;

7
src/util/useParams.ts Normal file
View File

@ -0,0 +1,7 @@
import { useParams as useParamsReach } from '@reach/router';
const useParams = <T extends unknown>(): T => {
return useParamsReach() as T;
};
export default useParams;

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from 'react';
const getWindowDimensions = () => {
const { innerWidth: width, innerHeight: height } = window;
return {
width,
height,
};
};
const useWindowDimensions = (): {
width: number;
height: number;
} => {
const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
useEffect(() => {
const handleResize = () => {
setWindowDimensions(getWindowDimensions());
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowDimensions;
};
export default useWindowDimensions;

41
tsconfig.json Normal file
View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
/* Specify ECMAScript target version */
"target": "es6",
/* Specify module code generation */
"module": "esnext",
/* Specify library files to be included in the compilation. */
"lib": [
"ESNext",
"DOM"
],
"experimentalDecorators": true,
/* Specify JSX code generation */
"jsx": "react",
/* Generate corresponding .map files */
"sourceMap": true,
/* Enable all strict type-checking options */
"strict": false,
/* Specify module resolution strategy */
"moduleResolution": "node",
/* Base directory to resolve non-absolute module names */
/*"baseUrl": "./",*/
/* Maps imports to locations - e.g. ~models will go to ./src/models */
/*"paths": {
"*": ["./*"]
},*/
/* List of folders to include type definitions from */
"typeRoots": [
"node_modules/@types"
],
/* allow import React instead of import * as React */
"allowSyntheticDefaultImports": true,
/* Emit interop between CommonJS and ES modules */
"esModuleInterop": true
},
"include": ["./src/**/*", "./*.d.ts"],
"exclude": [
"**/*.spec.ts",
"./node_modules/**/*.ts"
]
}

11
workspace.code-workspace Normal file
View File

@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
}

7409
yarn.lock Normal file

File diff suppressed because it is too large Load Diff