Bump version to 1.0.0

* Add ROT-Tool
* Switch to Lucide-React-Icons
* Prerender pages for SEO
* Add titles for SEO
* Add tags for SEO
* Add dark-mode (using device preferred mode)
This commit is contained in:
Kevin Kandlbinder 2021-05-19 16:22:38 +02:00
parent 21287fc568
commit 4c43ae8cc3
21 changed files with 891 additions and 74 deletions

View file

@ -1,29 +1,33 @@
{
"name": "kevins-data-toolbox",
"version": "0.1.0",
"version": "1.0.0",
"private": true,
"dependencies": {
"@loadable/component": "^5.15.0",
"@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",
"lucide-react": "^0.15.16",
"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-prerendered-component": "^1.2.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-snap": "^1.23.0",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"postbuild": "react-snap"
},
"eslintConfig": {
"extends": [

View file

@ -16,7 +16,20 @@
"toolList": "Liste von Werkzeugen",
"noresults": "Keine Ergebnisse.",
"categories": {
"everything": "Alles"
"everything": "Alles",
"cryptography": "Kryptografie"
},
"cryptography": {
"common": {
"cleartext": "Entschlüsselter Text",
"ciphertext": "Verschlüsselter Text"
},
"rot": {
"title": "ROT-N",
"description": "Die <wikipedia>ROT-Verschlüsselung</wikipedia>, auch oft als Caesar-Verschlüsselung bezeichnet, basiert auf der Idee, jeden Buchstaben um einen bestimmten Versatz zu verschieben, z.B. <pre>A => +13 => N</pre>",
"outOfRangeWarning": "ROT unterstützt nur Buchstaben des Grundalphabets (A-Z). Nummern und Umlaute werden nicht unterstützt und unverschlüsselt kopiert!",
"offset": "ROT-Versatz (oftmals 13)"
}
}
},
"about": {

View file

@ -16,7 +16,20 @@
"toolList": "List of tools",
"noresults": "No results have been found.",
"categories": {
"everything": "Everything"
"everything": "Everything",
"cryptography": "Cryptography"
},
"cryptography": {
"common": {
"cleartext": "Cleartext",
"ciphertext": "Ciphertext"
},
"rot": {
"title": "ROT-N",
"description": "The <wikipedia>ROT-cipher</wikipedia>, also commonly referred to as the Caesar-cipher, is based on the idea of ofsetting every letter of the alphabet by a certain amount, i.e. <pre>A => +13 => N</pre>",
"outOfRangeWarning": "ROT only supports letters of the basic alphabet (A-Z). Numbers and accented letters are not supported and will be copied as-is!",
"offset": "ROT-Offset (commonly 13)"
}
}
},
"about": {

View file

@ -1,4 +1,4 @@
import React, { lazy, Suspense } from "react";
import React, { Suspense } from "react";
import {
BrowserRouter as Router,
Switch,
@ -9,11 +9,12 @@ import Navigation from "./components/Navigation";
import * as styles from "./App.module.scss";
import NotFoundPage from "./pages/NotFound";
import ToolLoader from "./tools/ToolLoader";
import { Trans } from "react-i18next";
import prerenderedLoadable from "./helpers/prerenderedLoadable";
import {version} from "../package.json"
const HomePage = lazy(() => import('./pages/Home'));
const ToolsPage = lazy(() => import('./pages/Tools'));
const AboutPage = lazy(() => import('./pages/About'));
const HomePage = prerenderedLoadable(() => import('./pages/Home'));
const ToolsPage = prerenderedLoadable(() => import('./pages/Tools'));
const AboutPage = prerenderedLoadable(() => import('./pages/About'));
function App() {
@ -31,8 +32,8 @@ function App() {
<Route path="*" component={NotFoundPage} />
</Switch>
</Suspense>
<footer className={styles.footer}>CC-BY-4.0 Kevin Kandlbinder | v{version} | <a href="//kevink.dev/legal/about">Impressum</a></footer>
</div>
<footer className={styles.footer}>CC-BY-4.0 Kevin Kandlbinder | <a href="//kevink.dev/legal/about">Impressum</a></footer>
</Router>
</Suspense>
);

View file

@ -10,6 +10,10 @@
> * {
flex-shrink: 0;
}
> div {
flex-grow: 1;
}
}
.footer {

View file

@ -13,8 +13,53 @@ $layoutNavigationHeight: 50px;
}
@mixin boxStyle {
text-decoration: none;
box-shadow: 0 0 10px rgba(black, .25);
border: none;
border-radius: 10px;
background: none;
padding: $layoutPadding;
font: inherit;
color: inherit;
margin: $layoutPadding 0;
@media(prefers-color-scheme: dark) {
background: rgba(255, 255, 255, .05);
}
}
@mixin formStyles() {
input[type=text], input[type=password], textarea, input[type=number] {
@include boxStyle;
display: block;
&.center {
margin: $layoutPadding auto;
}
}
label {
display: block;
margin-top: $layoutPadding;
margin-bottom: (-$layoutPadding + 5px);
&.center {
text-align: center;
}
}
textarea {
width: 100%;
display: block;
}
}
.layoutBox {
@include layoutBox();
@include formStyles();
}
.title {

View file

@ -0,0 +1,27 @@
import React from "react";
import PropTypes from 'prop-types';
import * as styles from "./BoxMessage.module.scss";
const BoxMessage = (props) => {
return (
<div className={styles.boxMessage + " " + styles[props.color] + " " + (props.hideInPlace ? styles.hideInPlace : "")}>
{props.icon ? <div className={styles.icon}><props.icon/></div> : null}
<span className={styles.content}>{props.children}</span>
</div>
);
}
BoxMessage.defaultProps = {
"color": "blue",
"hideInPlace": false
}
BoxMessage.props = {
"children": PropTypes.array.isRequired,
"icon": PropTypes.object,
"color": PropTypes.string,
"hideInPlace": PropTypes.bool
};
export default BoxMessage;

View file

@ -0,0 +1,59 @@
@import "../common";
@mixin boxMessageColor($baseColor) {
$bgColor: adjust-color($baseColor, null, null, null, null, null, 30);
background-color: $bgColor;
.icon > svg {
color: $baseColor;
}
@media(prefers-color-scheme: dark) {
$bgColor: adjust-color($baseColor, null, null, null, null, null, -35);
background-color: $bgColor;
.icon > svg {
color: adjust-color($baseColor, null, null, null, null, null, 5);
}
}
}
@keyframes jump {
0% {transform: scale(.8);}
50% {transform: scale(1.1);}
100% {transform: scale(1);}
}
.boxMessage {
padding: $layoutPadding;
margin: $layoutPadding 0;
border-radius: 5px;
box-shadow: 0 0 10px rgba(black, .15);
display: flex;
transition: opacity .25s;
animation: jump .25s ease-in-out 1 normal both running;
.icon {
svg {
width: 30px;
height: 30px;
}
margin-right: $layoutPadding;
}
&.red {
@include boxMessageColor(#ff1c1c)
}
&.hideInPlace {
pointer-events: none;
opacity: 0;
animation: unset;
}
}

View file

@ -1,13 +1,17 @@
@import "../common";
.linkBox {
@include boxStyle;
padding: 30px;
color: $colorAccent;
width: 300px;
text-decoration: none;
box-shadow: 0 0 10px rgba(black, .25);
border-radius: 10px;
width: 250px;
margin: $layoutPadding;
@media(prefers-color-scheme: dark) {
background: rgba(255, 255, 255, .05);
}
.lbIcon {
svg {

View file

@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
import { useTranslation } from 'react-i18next';
import * as styles from "./Navigation.module.scss";
import { Globe } from "react-feather";
import { Globe } from "lucide-react";
import LanguageChooser from "./LanguageChooser";
const Navigation = () => {

View file

@ -0,0 +1,15 @@
import React from "react";
import loadable from "@loadable/component";
import { PrerenderedComponent } from "react-prerendered-component";
const prerenderedLoadable = dynamicImport => {
const LoadableComponent = loadable(dynamicImport);
return React.memo(props => (
// you can use the `.preload()` method from react-loadable or react-imported-component`
<PrerenderedComponent live={LoadableComponent.load()}>
<LoadableComponent {...props} />
</PrerenderedComponent>
));
};
export default prerenderedLoadable;

View file

@ -12,6 +12,13 @@ html, body, #root {
font-family: $fontFamily;
}
body {
@media(prefers-color-scheme: dark) {
background-color: #0c0c0c;
color: white;
}
}
a {
color: $colorAccent;
text-decoration: underline dotted currentColor;

View file

@ -3,11 +3,13 @@ import React from "react";
import * as styles from "./About.module.scss";
import { useTranslation, Trans } from 'react-i18next';
import { Helmet } from "react-helmet";
const AboutPage = () => {
const { t } = useTranslation();
return ([
<Helmet><title>{t("about.title")} | {t("site.title")}</title></Helmet>,
<div>
<div className={styles.layoutBox}>
<h1>{t("about.title")}</h1>

View file

@ -1,15 +1,17 @@
import React from "react";
import { List } from 'react-feather';
import { Binary, List } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import * as styles from "./Home.module.scss";
import LinkBox from "../components/LinkBox";
import { Helmet } from "react-helmet";
const HomePage = () => {
const { t } = useTranslation();
return ([
<Helmet><title>Home | {t("site.title")}</title></Helmet>,
<div className={styles.heroBox}>
<div className={styles.layoutBox}>
<span className={styles.heroPretitle}>{t("home.heroPretitle")}</span>
@ -23,6 +25,7 @@ const HomePage = () => {
<div className={styles.flexList}>
<LinkBox to={"/tools"} text={t("tools.categories.everything")} icon={List} />
<LinkBox to={"/tools/cryptography"} text={t("tools.categories.cryptography")} icon={Binary} />
{/*<LinkBox to={"/tools/osm"} text={"OSM"} icon={Map} />*/}
</div>
</div>

View file

@ -1,17 +1,19 @@
import React from "react";
import { Frown } from 'react-feather';
import { Frown } from 'lucide-react';
import * as styles from "./NotFound.module.scss";
import { useTranslation } from 'react-i18next';
import { Helmet } from "react-helmet";
const NotFoundPage = () => {
const { t } = useTranslation();
return ([
<Helmet><title>404: {t("system.notfound")}</title></Helmet>,
<div>
<div className={styles.layoutBox}>
<h1>{t("system.notfound")} <Frown/></h1>
<h1>404: {t("system.notfound")} <Frown/></h1>
</div>
</div>

View file

@ -1,6 +1,6 @@
import React from "react";
import { useParams } from "react-router-dom";
import * as icons from 'react-feather';
import * as icons from 'lucide-react';
import { useTranslation } from 'react-i18next';
@ -8,6 +8,7 @@ import * as styles from "./Tools.module.scss";
import LinkBox from "../components/LinkBox";
import { tools } from "../tools/tools.json";
import { Helmet } from "react-helmet";
const ToolsPage = () => {
const { t } = useTranslation();
@ -21,12 +22,14 @@ const ToolsPage = () => {
});
return ([
<Helmet><title>{t("tools.toolList")} | {t("site.title")}</title></Helmet>,
<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"} text={t("tools.categories.everything")} icon={icons.List} small={true} highlight={category == null} />
<LinkBox to={"/tools/cryptography"} text={t("tools.categories.cryptography")} icon={icons.Binary} small={true} highlight={category === "cryptography"} />
{/*<LinkBox to={"/tools/osm"} text={"OSM"} icon={icons["Map"]} small={true} highlight={category === "osm"} />*/}
</div>

View file

@ -1,8 +1,11 @@
import React, { lazy } from "react";
import React from "react";
import { useParams } from "react-router";
import prerenderedLoadable from "../helpers/prerenderedLoadable";
import NotFoundPage from "../pages/NotFound";
const HomePage = lazy(() => import('../pages/Home'));
const HomePage = prerenderedLoadable(() => import('../pages/Home'));
const RotTool = prerenderedLoadable(() => import('./cyphers_and_cryptography/rot/RotTool'));
const ToolLoader = () => {
const {tool} = useParams();
@ -11,6 +14,9 @@ const ToolLoader = () => {
case "test":
return <HomePage/>;
case "rot":
return <RotTool/>;
default:
return <NotFoundPage/>;
}

View file

@ -0,0 +1,80 @@
import React, { useEffect, useState } from "react";
import { AlertOctagon } from "lucide-react";
import { useTranslation, Trans } from "react-i18next";
import BoxMessage from "../../../components/BoxMessage";
import * as styles from "./RotTool.module.scss"
import { Helmet } from "react-helmet";
const RotTool = () => {
const { t } = useTranslation();
let [input, setInput] = useState("");
let [output, setOutput] = useState("");
let [offset, setOffset] = useState(13);
let [reversed, setReversed] = useState(false)
let [outOfRangeWarning, setOutOfRangeWarning] = useState(false)
useEffect(() => {
let actualOffset = offset;
if(reversed) actualOffset = -offset;
let min = 97; // This is a
let max = 122; // This is z
let range = max - min; // The length of the alphabet
let rotInput = reversed ? output.toLowerCase() : input.toLowerCase();
rotInput = rotInput.split('');
let hasOutOfRange = false;
let rotOut = rotInput.map((char) => {
let charCode = char.charCodeAt(0);
if(charCode > max || charCode < min) {
hasOutOfRange = true;
return char;
}
charCode += actualOffset;
while(charCode > max) charCode -= range;
while(charCode < min) charCode += range;
return String.fromCharCode(charCode);
})
setOutOfRangeWarning(hasOutOfRange);
rotOut = rotOut.join('').toUpperCase();
reversed ? setInput(rotOut) : setOutput(rotOut)
}, [input, output, reversed, offset])
return (
<div>
<Helmet>
<title>{t("tools.cryptography.rot.title")} | {t("site.title")}</title>
<meta name="keywords" content="ROT, encryption, decryption, verschlüsselung, entschlüsselung, ROT-13, Caesar-cipher, cipher, caesar, cäsar-chiffre, tool" />
</Helmet>
<div className={styles.layoutBox}>
<h1>{t("tools.cryptography.rot.title")}</h1>
<p><Trans i18nKey={"tools.cryptography.rot.description"} components={{wikipedia: <a href="https://en.wikipedia.org/wiki/ROT13">xxx</a>, pre: <pre/>}} /></p>
<BoxMessage icon={AlertOctagon} color="red" hideInPlace={!outOfRangeWarning}>{t("tools.cryptography.rot.outOfRangeWarning")}</BoxMessage>
<label for="rot-input">{t("tools.cryptography.common.cleartext")}</label>
<textarea id="rot-input" placeholder={t("tools.cryptography.common.cleartext")} onChange={(e) => {setReversed(false); setInput(e.currentTarget.value.toUpperCase());}} value={input}></textarea>
<label for="rot-offset" className={styles.center}>{t("tools.cryptography.rot.offset")}</label>
<input type="number" id="rot-offset" value={offset} onChange={(e) => {setOffset(parseInt(e.currentTarget.value))}} className={styles.center} />
<label for="rot-output">{t("tools.cryptography.common.ciphertext")}</label>
<textarea id="rot-output" placeholder={t("tools.cryptography.common.ciphertext")} onChange={(e) => {setReversed(true); setOutput(e.currentTarget.value.toUpperCase());}} value={output}></textarea>
</div>
</div>
)
}
export default RotTool;

View file

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

View file

@ -9,12 +9,13 @@
"hidden": true
},
{
"name": "Test02",
"name": "ROT-N",
"external": false,
"urlname": "test",
"icon": "Smile",
"category": "something",
"hidden": true
"urlname": "rot",
"icon": "PlusSquare",
"category": "cryptography",
"hidden": false,
"keywords": "rot, rot-n, caesar, rotation, cryptography, encryption, decryption"
}
]
}

619
yarn.lock

File diff suppressed because it is too large Load diff