Bootstrap Kevin's Data-Toolbox

This commit is contained in:
Kevin Kandlbinder 2021-04-09 23:42:32 +02:00
parent 4da936864f
commit 3f4d6da00b
30 changed files with 1255 additions and 120 deletions

View file

@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View file

@ -1,24 +1,37 @@
import logo from './logo.svg';
import './App.css';
import React, { lazy, Suspense } from "react";
import {
BrowserRouter as Router,
Switch,
Route
} from "react-router-dom";
import Navigation from "./components/Navigation";
import * as styles from "./App.module.scss";
import NotFoundPage from "./pages/NotFound";
import ToolLoader from "./tools/ToolLoader";
const HomePage = lazy(() => import('./pages/Home'));
const ToolsPage = lazy(() => import('./pages/Tools'));
const AboutPage = lazy(() => import('./pages/About'));
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<Suspense fallback="Kevin's Data-Toolbox is loading...">
<Router>
<div className={styles.appContainer}>
<Navigation/>
<Suspense fallback="Kevin's Data-Toolbox is loading...">
<Switch>
<Route path="/about" component={AboutPage} />
<Route path="/tools/:category?" component={ToolsPage} />
<Route path="/tool/:tool" component={ToolLoader} />
<Route path="/" exact component={HomePage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</Suspense>
</div>
</Router>
</Suspense>
);
}

13
src/App.module.scss Normal file
View file

@ -0,0 +1,13 @@
@import "./common";
.appContainer {
display: flex;
flex-direction: column;
min-height: 100vh;
padding-top: $layoutNavigationHeight;
> * {
flex-shrink: 0;
}
}

View file

@ -1,8 +1,9 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
/*test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
const linkElement = screen.getByText(/about/i);
expect(linkElement).toBeInTheDocument();
});
*/

36
src/_common.scss Normal file
View file

@ -0,0 +1,36 @@
$fontFamily: 'Open Sans', sans-serif;
$colorAccent: #f58428;
$layoutWidth: 1200px;
$layoutPadding: 20px;
$layoutNavigationHeight: 50px;
@mixin layoutBox() {
max-width: $layoutWidth;
padding: 0 $layoutPadding;
margin: 0 auto;
}
.layoutBox {
@include layoutBox();
}
.title {
display: block;
font-weight: 800;
font-size: 3em;
padding: 20px 0;
}
.flexList {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
> * {
margin: $layoutPadding;
}
}

View file

@ -0,0 +1,25 @@
import React from "react";
import ReactDOM from "react-dom";
import { Link } from "react-router-dom";
import { useTranslation } from 'react-i18next';
import * as styles from "./LanguageChooser.module.scss";
const LanguageChooser = (props) => {
const { t, i18n } = useTranslation();
return ReactDOM.createPortal(
<div className={styles.lChooser + " " + (props.active ? styles.active : "")}>
<div>
<span className={styles.title}>{t("system.language")}</span>
<Link to={"#"} onClick={() => {i18n.changeLanguage("en"); props.onDone()}}>English</Link>
<Link to={"#"} onClick={() => {i18n.changeLanguage("de"); props.onDone()}}>Deutsch</Link>
</div>
</div>,
document.body
);
}
export default LanguageChooser;

View file

@ -0,0 +1,45 @@
@import "../common";
.lChooser {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background-color: #000000e0;
backdrop-filter: blur(5px);
color: white;
opacity: 0;
pointer-events: none;
transition: opacity .25s;
&.active {
opacity: 1;
pointer-events: visible;
}
> div {
position: absolute;
top: 10%;
left: 50%;
transform: translate(-50%, 0);
display: flex;
flex-direction: column;
}
a {
align-items: center;
justify-content: center;
display: flex;
color: inherit;
text-decoration: none;
padding: $layoutPadding;
}
}

35
src/components/LinkBox.js Normal file
View file

@ -0,0 +1,35 @@
import React from "react";
import { Link } from "react-router-dom";
import PropTypes from 'prop-types';
import * as styles from "./LinkBox.module.scss";
const LinkBox = (props) => {
return (!props.external ?
<Link className={styles.linkBox + (props.small ? " "+styles.small : "") + (props.highlight ? " "+styles.highlight : "")} to={props.to}>
<div className={styles.lbIcon}><props.icon/></div>
<span className={styles.lbText}>{props.text}</span>
</Link> :
<a className={styles.linkBox + (props.small ? " "+styles.small : "") + (props.highlight ? " "+styles.highlight : "")} href={props.to}>
<div className={styles.lbIcon}><props.icon/></div>
<span className={styles.lbText}>{props.text}</span>
</a>
);
}
LinkBox.defaultProps = {
"small": false,
"highlight": false,
"external": false
}
LinkBox.props = {
"to": PropTypes.string,
"text": PropTypes.string.isRequired,
"icon": PropTypes.object.isRequired,
"small": PropTypes.bool,
"highlight": PropTypes.bool,
"external": PropTypes.bool
};
export default LinkBox;

View file

@ -0,0 +1,56 @@
@import "../common";
.linkBox {
padding: 30px;
color: $colorAccent;
text-decoration: none;
box-shadow: 0 0 10px rgba(black, .25);
border-radius: 10px;
width: 250px;
.lbIcon {
svg {
width: 50px;
height: 50px;
}
margin-bottom: 10px;
}
.lbText {
font-size: 2em;
font-weight: 700;
}
&.highlight {
background-color: $colorAccent;
color: white;
}
&.small {
width: unset;
border-radius: 100px;
display: flex;
padding: 5px 15px;
line-height: 25px;
align-items: center;
.lbIcon {
height: 25px;
svg {
width: 20px;
height: 20px;
margin: 2.5px;
}
margin-bottom: 0;
margin-right: 5px;
}
.lbText {
font-size: 1em;
font-weight: 200;
}
}
}

View file

@ -0,0 +1,29 @@
import React, { useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from 'react-i18next';
import * as styles from "./Navigation.module.scss";
import { Globe } from "react-feather";
import LanguageChooser from "./LanguageChooser";
const Navigation = () => {
const { t } = useTranslation();
const [langChooserActive, setLangChooserActive] = useState(false);
return (
<div className={styles.navigation}>
<nav>
<Link to={"/"}>{t("site.title")}</Link>
<span className={styles.spacer}></span>
<Link to={"/tools"}>{t("site.navigation.tools")}</Link>
<Link to={"/about"}>{t("site.navigation.about")}</Link>
<Link to={"#"} onClick={() => {setLangChooserActive(true)}}><Globe/></Link>
<LanguageChooser active={langChooserActive} onDone={() => {setLangChooserActive(false)}} />
</nav>
</div>
);
}
export default Navigation;

View file

@ -0,0 +1,39 @@
@import "../common";
.navigation {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: $layoutNavigationHeight;
background-color: #1c1c1c;
z-index: 100;
@supports(backdrop-filter: blur(5px)) {
background-color: #000000e0;
backdrop-filter: blur(5px);
}
> nav {
max-width: $layoutWidth;
margin: 0 auto;
display: flex;
height: $layoutNavigationHeight;
align-items: stretch;
.spacer {
flex-grow: 1;
}
> a {
padding: 0 $layoutPadding;
align-items: center;
display: flex;
color: white;
text-decoration: none;
}
}
}

31
src/i18n.js Normal file
View file

@ -0,0 +1,31 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
// don't want to use this?
// have a look at the Quick start guide
// for passing in lng and translations on init
i18n
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
// learn more: https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
fallbackLng: 'en',
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
}
});
export default i18n;

View file

@ -1,13 +0,0 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View file

@ -1,9 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import './index.scss';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './i18n';
ReactDOM.render(
<React.StrictMode>
<App />

18
src/index.scss Normal file
View file

@ -0,0 +1,18 @@
@import "./common";
* {
box-sizing: border-box;
}
html, body, #root {
min-height: 100vh;
margin: 0;
padding: 0;
font-family: $fontFamily;
}
a {
color: $colorAccent;
text-decoration: underline dotted currentColor;
}

27
src/pages/About.js Normal file
View file

@ -0,0 +1,27 @@
import React from "react";
import * as styles from "./About.module.scss";
import { useTranslation, Trans } from 'react-i18next';
const AboutPage = () => {
const { t } = useTranslation();
return ([
<div>
<div className={styles.layoutBox}>
<h1>{t("about.title")}</h1>
<p>{t("about.p1")}</p>
<p>{t("about.p2")}</p>
<h2>{t("about.morebyme")}</h2>
<p><Trans i18nKey={"about.visitKevinKdev"}> <a href="https://kevink.dev"> </a> </Trans></p>
</div>
</div>
]);
}
export default AboutPage;

View file

@ -0,0 +1 @@
@import "../common";

33
src/pages/Home.js Normal file
View file

@ -0,0 +1,33 @@
import React from "react";
import { List } from 'react-feather';
import { useTranslation } from 'react-i18next';
import * as styles from "./Home.module.scss";
import LinkBox from "../components/LinkBox";
const HomePage = () => {
const { t } = useTranslation();
return ([
<div className={styles.heroBox}>
<div className={styles.layoutBox}>
<span className={styles.heroPretitle}>{t("home.heroPretitle")}</span>
<span className={styles.heroTitle}>{t("home.heroTitle")}</span>
<span className={styles.heroSubtitle}>{t("home.heroSubtitle")}</span>
</div>
</div>,
<div className={styles.categoryBox}>
<div className={styles.layoutBox}>
<span className={styles.title}>{t("tools.toolCategories")}</span>
<div className={styles.flexList}>
<LinkBox to={"/tools"} text={t("tools.categories.everything")} icon={List} />
{/*<LinkBox to={"/tools/osm"} text={"OSM"} icon={Map} />*/}
</div>
</div>
</div>
]);
}
export default HomePage;

View file

@ -0,0 +1,41 @@
@import "../common";
.heroBox {
margin-top: -$layoutNavigationHeight;
background-color: #1c1c1c;
background-image: url(https://source.unsplash.com/uq5RMAZdZG4/1920x1080);
> div {
padding-top: 100px;
padding-bottom: 100px;
min-height: 600px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
span {
color: white;
text-shadow: 0 0 10px black, 0 0 10px black;
&.heroPretitle {
font-size: 2em;
font-weight: 100;
margin-bottom: -20px;
}
&.heroTitle {
font-size: 4em;
font-weight: 800;
}
&.heroSubtitle {
font-size: 1.5em;
font-weight: 400;
margin-bottom: -20px;
}
}
}
}

21
src/pages/NotFound.js Normal file
View file

@ -0,0 +1,21 @@
import React from "react";
import { Frown } from 'react-feather';
import * as styles from "./NotFound.module.scss";
import { useTranslation } from 'react-i18next';
const NotFoundPage = () => {
const { t } = useTranslation();
return ([
<div>
<div className={styles.layoutBox}>
<h1>{t("system.notfound")} <Frown/></h1>
</div>
</div>
]);
}
export default NotFoundPage;

View file

@ -0,0 +1 @@
@import "../common";

45
src/pages/Tools.js Normal file
View file

@ -0,0 +1,45 @@
import React from "react";
import { useParams } from "react-router-dom";
import * as icons from 'react-feather';
import { useTranslation } from 'react-i18next';
import * as styles from "./Tools.module.scss";
import LinkBox from "../components/LinkBox";
import { tools } from "../tools/tools.json";
const ToolsPage = () => {
const { t } = useTranslation();
let { category } = useParams();
if(category) category = category.toLowerCase();
let toolList = tools.filter((tool) => {
return !tool.hidden && (category == null || tool.category === category);
});
return ([
<div className={styles.categoryBox}>
<div className={styles.layoutBox}>
<span className={styles.title}>{t("tools.toolList")}</span>
<div className={styles.flexList}>
<LinkBox to={"/tools"} text={t("tools.categories.everything")} icon={icons["List"]} small={true} highlight={category == null} />
{/*<LinkBox to={"/tools/osm"} text={"OSM"} icon={icons["Map"]} small={true} highlight={category === "osm"} />*/}
</div>
<div className={styles.flexList}>
{toolList.map((tool, i) => {
return (<LinkBox key={"tool"+i} external={tool.external} to={tool.external ? tool.url : "/tool/"+tool.urlname} text={tool.name} icon={icons[tool.icon]} />);
})}
{toolList.length === 0 ? <span>{t("tools.noresults")}</span> : null}
</div>
</div>
</div>
]);
}
export default ToolsPage;

View file

@ -0,0 +1 @@
@import "../common";

19
src/tools/ToolLoader.js Normal file
View file

@ -0,0 +1,19 @@
import React, { lazy } from "react";
import { useParams } from "react-router";
import NotFoundPage from "../pages/NotFound";
const HomePage = lazy(() => import('../pages/Home'));
const ToolLoader = () => {
const {tool} = useParams();
switch(tool) {
case "test":
return <HomePage/>;
default:
return <NotFoundPage/>;
}
}
export default ToolLoader;

20
src/tools/tools.json Normal file
View file

@ -0,0 +1,20 @@
{
"tools": [
{
"name": "Test01",
"external": true,
"url": "https://kevink.dev",
"icon": "Smile",
"category": "osm",
"hidden": true
},
{
"name": "Test02",
"external": false,
"urlname": "test",
"icon": "Smile",
"category": "something",
"hidden": true
}
]
}