feature: base64 tool page

This commit is contained in:
Alikia2x 2024-05-05 16:28:12 +08:00
parent 969ed948b5
commit ff0d05542d
8 changed files with 208 additions and 1 deletions

View File

@ -1,10 +1,77 @@
"use client";
import Switcher from "@/components/switcher";
import Notice from "@/components/tools/notice";
import base64ToHex from "@/lib/base64ToHex";
import copyToClipboard from "@/lib/copy";
import normalizeHex from "@/lib/normalizeHex";
import { validBase64 } from "@/lib/onesearch/baseCheck";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { utoa, atou } from "unicode-encode";
export default function Base64() {
const t = useTranslations("tools");
const [mode, setMode] = useState("Encode");
const [message, setMessage] = useState("");
const [messageResult, setMessageResult] = useState("");
const [isHex, setHex] = useState(false);
const [info, setInfo] = useState("");
const [type, setType] = useState("");
useEffect(() => {
setHex(false);
if (mode == "Encode") {
setMessageResult(utoa(message));
} else {
if (validBase64(message)) {
try {
setMessageResult(atou(message));
} catch (e) {
setMessageResult(normalizeHex(base64ToHex(message)));
setHex(true);
}
} else if (message.trim() !== "") {
setMessageResult("Invalid Base64");
} else {
setMessageResult("");
}
}
});
return (
<div>
<h1 className="text-3xl font-semibold">{t("base64.title")}</h1>
<Switcher items={["Encode", "Decode"]} selected={mode} setSelected={setMode} class="mt-4" />
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
className="w-full h-80 mt-4 p-4 rounded-lg bg-zinc-100 dark:bg-zinc-800 resize-none outline-none duration-200 transition-colors-opacity border-2 border-transparent focus:border-zinc-600 dark:focus:border-zinc-300"
/>
<div className="w-full h-12 mt-4">
<span className="w-fit text-2xl font-bold leading-10">Result:</span>
<button
onClick={() => {
copyToClipboard(messageResult);
setType("info");
setInfo("Copied");
setTimeout(() => {
setInfo("");
setType("");
}, 3000);
}}
className="absolute right-0 w-fit h-10 rounded-md leading-10 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-300 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-300"
>
Copy
</button>
</div>
<div
className={`empty:py-0 mt-6 w-full h-fit rounded-md leading-10 bg-zinc-100 dark:bg-zinc-800 py-2 px-5 z-10 cursor-pointer duration-100 break-all ${
isHex ? "font-mono" : ""
}`}
>
{messageResult.length > 0 ? messageResult : "Waiting for input..."}
</div>
<Notice type={type} info={info} class="mt-4" />
<Notice type="info" info="HI." class="mt-4" />
</div>
);
}

31
components/switcher.tsx Normal file
View File

@ -0,0 +1,31 @@
'use client';
import React, { useState, useEffect, useRef } from "react";
export default function Switcher(props: { items: string[]; selected: string, setSelected: Function, class?: string }) {
const selectedRef = useRef(null);
const [selectedCoordinate, setSelectedCoordinate] = useState(0);
const [selectedWidth, setSelectedWidth] = useState(0);
useEffect(() => {
if (selectedRef.current){
setSelectedCoordinate((selectedRef.current as HTMLElement)?.offsetLeft);
setSelectedWidth((selectedRef.current as HTMLElement)?.getBoundingClientRect().width);
}
}, [props.selected]);
return (
<div className={`relative w-fit h-12 px-1 flex rounded-lg bg-zinc-100 dark:bg-zinc-800 z-0 ${props.class}`}>
{props.items.map((item, index) => (
<div
key={index}
className="relative mt-[0.375rem] rounded-md w-fit h-9 leading-9 px-4 mx-1 z-20 cursor-pointer duration-100"
ref={item == props.selected ? selectedRef : null}
onClick={() => props.setSelected(item)}
>
{item}
</div>
))}
<div className="absolute mt-[0.375rem] rounded-md h-9 bg-zinc-300 dark:bg-zinc-600 z-10 duration-250 ease-[cubic-bezier(.15,.16,.2,1.2)]" style={{ left: selectedCoordinate, width: selectedWidth }}></div>
</div>
);
}

View File

@ -0,0 +1,32 @@
"use client";
import React, { useState, useEffect, useRef } from "react";
import { Icon } from "@iconify-icon/react";
const typeToColor: Record<string, string> = {
success: "bg-green-500",
info: "bg-blue-500",
warning: "bg-orange-500",
error: "bg-red-500"
};
const typeToIcon: Record<string, string> = {
success: "material-symbols:check-circle",
info: "material-symbols:info",
warning: "material-symbols:warning",
error: "material-symbols:error"
};
export default function Notice(props: { type: string; info: string; class?: string }) {
if (props.type && props.info)
return (
<div
className={`relative ${props.class} ${
typeToColor[props.type]
} rounded-md w-full min-h-9 h-fit empty:px-0 px-4 mx-1 z-20 cursor-pointer duration-100`}
>
<Icon className="text-2xl mt-3" icon={typeToIcon[props.type]} />
<span className="">{props.info}</span>
</div>
);
}

15
lib/base64ToHex.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Converts a base64 string to a hexadecimal string.
*
* @param {string} base64String - The base64 string to convert.
* @return {string} The hexadecimal representation of the base64 string.
*/
export default function base64ToHex(base64String: string): string {
const raw = atob(base64String);
let result = "";
for (let i = 0; i < raw.length; i++) {
const hex = raw.charCodeAt(i).toString(16);
result += hex.length === 2 ? hex : "0" + hex;
}
return result.toUpperCase();
}

16
lib/normalizeHex.ts Normal file
View File

@ -0,0 +1,16 @@
/**
* A description of the entire function.
*
* @param {string} hexString - The input hexadecimal string to normalize.
* @return {string} The normalized hexadecimal string.
*/
export default function normalizeHex(hexString: string): string {
const chunkSize = 4;
const chunks: string[] = [];
for (let i = 0; i < hexString.length; i += chunkSize) {
chunks.push(hexString.substr(i, chunkSize));
}
return chunks.join(' ');
}

View File

@ -24,10 +24,12 @@
"search-engine-autocomplete": "^0.4.3",
"tailwind-merge": "^2.3.0",
"ts-node": "^10.9.2",
"unicode-encode": "^1.4.2",
"valid-url": "^1.0.9",
"validate-color": "^2.2.4"
},
"devDependencies": {
"@iconify-icon/react": "^2.1.0",
"@jest/globals": "^29.7.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.3.1",

View File

@ -47,6 +47,9 @@ dependencies:
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@20.12.7)(typescript@5.4.5)
unicode-encode:
specifier: ^1.4.2
version: 1.4.2
valid-url:
specifier: ^1.0.9
version: 1.0.9
@ -55,6 +58,9 @@ dependencies:
version: 2.2.4
devDependencies:
'@iconify-icon/react':
specifier: ^2.1.0
version: 2.1.0(react@18.3.0)
'@jest/globals':
specifier: ^29.7.0
version: 29.7.0
@ -516,6 +522,19 @@ packages:
tslib: 2.6.2
dev: false
/@iconify-icon/react@2.1.0(react@18.3.0):
resolution: {integrity: sha512-OuEsW5Y474rg3WlseLFQ0uuJjnyk1DhLN1Ire5JGjF4sF8/rNxGJDLSItEogRcKuUbL+zzuoBsaTUVVInuixRA==}
peerDependencies:
react: '>=16'
dependencies:
iconify-icon: 2.1.0
react: 18.3.0
dev: true
/@iconify/types@2.0.0:
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
dev: true
/@internationalized/date@3.5.2:
resolution: {integrity: sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==}
dependencies:
@ -3720,6 +3739,12 @@ packages:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: true
/atob@2.1.2:
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
engines: {node: '>= 4.5.0'}
hasBin: true
dev: false
/atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
engines: {node: '>=8.0.0'}
@ -3872,6 +3897,12 @@ packages:
node-int64: 0.4.0
dev: true
/btoa@1.2.1:
resolution: {integrity: sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==}
engines: {node: '>= 0.4.0'}
hasBin: true
dev: false
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
@ -4664,6 +4695,12 @@ packages:
engines: {node: '>=10.17.0'}
dev: true
/iconify-icon@2.1.0:
resolution: {integrity: sha512-lto4XU3bwTQnb+D/CsJ4dWAo0aDe+uPMxEtxyOodw9l7R9QnJUUab3GCehlw2M8mDHdeUu/ufx8PvRQiJphhXg==}
dependencies:
'@iconify/types': 2.0.0
dev: true
/iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@ -6730,6 +6767,13 @@ packages:
/undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
/unicode-encode@1.4.2:
resolution: {integrity: sha512-xwGGPy/masrMFrf7c+jlGj0SOO7Z9LpC6wAZbAaAM6Dyz/Azm1iuHDpWHDPot8IDpmhN9pGhUqZkE+5ykOKAjg==}
dependencies:
atob: 2.1.2
btoa: 1.2.1
dev: false
/universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}