feature: base64 tool page
This commit is contained in:
parent
969ed948b5
commit
ff0d05542d
@ -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
31
components/switcher.tsx
Normal 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>
|
||||
);
|
||||
}
|
32
components/tools/notice.tsx
Normal file
32
components/tools/notice.tsx
Normal 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
15
lib/base64ToHex.ts
Normal 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
16
lib/normalizeHex.ts
Normal 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(' ');
|
||||
}
|
@ -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",
|
||||
|
@ -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'}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import {nextui} from "@nextui-org/react";
|
||||
import { nextui } from "@nextui-org/react";
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
|
Loading…
Reference in New Issue
Block a user