mirror of
https://github.com/Unkn0wnCat/data-toolbox-site.git
synced 2025-04-29 18:16:21 +02:00
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:
parent
21287fc568
commit
4c43ae8cc3
21 changed files with 891 additions and 74 deletions
10
package.json
10
package.json
|
@ -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": [
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
13
src/App.js
13
src/App.js
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
|
|
|
@ -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 {
|
||||
|
|
27
src/components/BoxMessage.js
Normal file
27
src/components/BoxMessage.js
Normal 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;
|
59
src/components/BoxMessage.module.scss
Normal file
59
src/components/BoxMessage.module.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
15
src/helpers/prerenderedLoadable.js
Normal file
15
src/helpers/prerenderedLoadable.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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/>;
|
||||
}
|
||||
|
|
80
src/tools/cyphers_and_cryptography/rot/RotTool.js
Normal file
80
src/tools/cyphers_and_cryptography/rot/RotTool.js
Normal 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;
|
|
@ -0,0 +1 @@
|
|||
@import "../../../common";
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Add table
Reference in a new issue