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

@ -6,8 +6,16 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"i18next": "^20.2.1",
"i18next-browser-languagedetector": "^6.1.0",
"i18next-http-backend": "^1.2.1",
"node-sass": "^5.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-feather": "^2.0.9",
"react-helmet": "^6.1.0",
"react-i18next": "^11.8.12",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"web-vitals": "^1.0.1"
},

View file

@ -7,37 +7,19 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
content="Tools crafted for your enjoyment."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>Kevin's Data-Toolbox</title>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700;800&display=swap" rel="stylesheet">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<noscript>You need to enable JavaScript to run Kevin's Data-Toolbox.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
Hello you source-dwelling human. If you like source-code, you should check out my GitHub-Account at https://github.com/Unkn0wnCat
-->
</body>
</html>

View file

@ -0,0 +1,34 @@
{
"site": {
"title": "Kevins Datenkasten",
"navigation": {
"tools": "Werkzeuge",
"about": "Über"
}
},
"home": {
"heroPretitle": "Kevins",
"heroTitle": "Datenkasten",
"heroSubtitle": "Dein 1-Stopp-Daten-Shop!"
},
"tools": {
"toolCategories": "Kategorien",
"toolList": "Liste von Werkzeugen",
"noresults": "Keine Ergebnisse.",
"categories": {
"everything": "Alles"
}
},
"about": {
"title": "Über Kevins Datenkasten",
"p1": "Kevins Datenkasten ist meine Kollektion von kleinen, nützlichen Werkzeugen. Schau doch mal ob du was nüzliches findest!",
"p2": "Ich werde mehr Werkzeuge hinzufügen sobald ich diese fertig habe, also schau regelmäßig wieder rein!",
"morebyme": "Mehr von mir",
"visitKevinKdev": "Schau dir meine Website unter <1>KevinK.dev</1> an!"
},
"system": {
"notfound": "Seite nicht gefunden",
"language": "Sprache"
}
}

View file

@ -0,0 +1,34 @@
{
"site": {
"title": "Kevin's Data-Toolbox",
"navigation": {
"tools": "Tools",
"about": "About"
}
},
"home": {
"heroPretitle": "Kevin's",
"heroTitle": "Data-Toolbox",
"heroSubtitle": "Your One-Stop-Data-Shop!"
},
"tools": {
"toolCategories": "Categories",
"toolList": "List of tools",
"noresults": "No results have been found.",
"categories": {
"everything": "Everything"
}
},
"about": {
"title": "About Kevin's Data-Toolbox",
"p1": "Kevin's Data-Toolbox is my collection of useful small tools. Feel free to look through them to see if there anything is of use to you!",
"p2": "There will be more tools added over time as I create them, so check back regularly to learn about new tools!",
"morebyme": "More By Me",
"visitKevinKdev": "Check out my website <1>KevinK.dev</1>!"
},
"system": {
"notfound": "Page Not Found",
"language": "Language"
}
}

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
}
]
}

624
yarn.lock

File diff suppressed because it is too large Load diff