Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions src/components/dev/BiConverter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ import { Alert } from '@/components/ui/Alert';
import { LoadFileButton, fileExt } from '@/components/ui/LoadFileButton';
import { CodeBlock, type CodeLanguage } from '@/components/ui/CodeBlock';
import { DownloadTextButton } from '@/components/ui/DownloadTextButton';
import type { Lang } from '@/i18n/config';

const TR: Record<Lang, {
loadFile: (left: string, right: string) => string;
input: string;
clear: string;
result: string;
conversionFailed: string;
}> = {
en: {
loadFile: (left, right) => `Load ${left} / ${right} file`,
input: 'Input',
clear: 'Clear',
result: 'Result',
conversionFailed: 'Conversion failed',
},
id: {
loadFile: (left, right) => `Muat file ${left} / ${right}`,
input: 'Input',
clear: 'Bersihkan',
result: 'Hasil',
conversionFailed: 'Konversi gagal',
},
};

const MIME_BY_EXT: Record<string, string> = {
json: 'application/json',
Expand All @@ -31,12 +55,15 @@ export interface BiConverterProps {
rightExts?: string[];
/** highlight.js language for the right-hand format's output (left is JSON). */
rightLang?: CodeLanguage;
/** UI language; defaults to English. */
lang?: Lang;
}

type Mode = 'toRight' | 'toLeft';

/** A two-way text converter: paste input, pick a direction, copy the result. */
export function BiConverter({ leftLabel, rightLabel, toRight, toLeft, placeholder, fileAccept, rightExts = [], rightLang = 'plaintext' }: BiConverterProps) {
export function BiConverter({ leftLabel, rightLabel, toRight, toLeft, placeholder, fileAccept, rightExts = [], rightLang = 'plaintext', lang = 'en' }: BiConverterProps) {
const t = TR[lang] ?? TR.en;
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [error, setError] = useState('');
Expand All @@ -53,7 +80,7 @@ export function BiConverter({ leftLabel, rightLabel, toRight, toLeft, placeholde
setOutput(next === 'toRight' ? toRight(source) : toLeft(source));
} catch (e) {
setOutput('');
setError(e instanceof Error ? e.message : 'Conversion failed');
setError(e instanceof Error ? e.message : t.conversionFailed);
}
};

Expand All @@ -80,11 +107,11 @@ export function BiConverter({ leftLabel, rightLabel, toRight, toLeft, placeholde
<div className="space-y-4">
{fileAccept && (
<div className="flex justify-end">
<LoadFileButton onLoad={loadFile} accept={fileAccept} label={`Load ${leftLabel} / ${rightLabel} file`} />
<LoadFileButton onLoad={loadFile} accept={fileAccept} label={t.loadFile(leftLabel, rightLabel)} />
</div>
)}
<TextArea
label="Input"
label={t.input}
value={input}
onChange={e => onInput(e.target.value)}
placeholder={placeholder}
Expand All @@ -99,7 +126,7 @@ export function BiConverter({ leftLabel, rightLabel, toRight, toLeft, placeholde
{rightLabel} → {leftLabel}
</Button>
<Button variant="ghost" onClick={clear}>
Clear
{t.clear}
</Button>
</div>

Expand All @@ -108,7 +135,7 @@ export function BiConverter({ leftLabel, rightLabel, toRight, toLeft, placeholde
{output && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Result</span>
<span className="text-sm font-medium text-muted-foreground">{t.result}</span>
<div className="flex gap-2">
<DownloadTextButton
text={output}
Expand Down
50 changes: 42 additions & 8 deletions src/islands/dev/Base64.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,44 @@ import { Button } from '@/components/ui/Button';
import { CopyButton } from '@/components/ui/CopyButton';
import { Alert } from '@/components/ui/Alert';
import { encodeBase64, decodeBase64 } from '@/tools/dev/base64.lib';
import type { Lang } from '@/i18n/config';

type Mode = 'encode' | 'decode';

export default function Base64() {
const TR: Record<Lang, {
inputLabel: string;
placeholder: string;
encode: string;
decode: string;
clear: string;
invalidBase64: string;
encodingFailed: string;
result: string;
}> = {
en: {
inputLabel: 'Input',
placeholder: 'Text to encode, or Base64 to decode',
encode: 'Encode →',
decode: '← Decode',
clear: 'Clear',
invalidBase64: 'Invalid Base64 input',
encodingFailed: 'Encoding failed',
result: 'Result',
},
id: {
inputLabel: 'Input',
placeholder: 'Teks untuk di-encode, atau Base64 untuk di-decode',
encode: 'Encode →',
decode: '← Decode',
clear: 'Bersihkan',
invalidBase64: 'Input Base64 tidak valid',
encodingFailed: 'Encoding gagal',
result: 'Hasil',
},
};

export default function Base64({ lang = 'en' }: { lang?: Lang }) {
const t = TR[lang] ?? TR.en;
const [input, setInput] = useState('');
const [output, setOutput] = useState('');
const [error, setError] = useState('');
Expand All @@ -24,7 +58,7 @@ export default function Base64() {
setOutput(mode === 'encode' ? encodeBase64(source) : decodeBase64(source));
} catch {
setOutput('');
setError(mode === 'decode' ? 'Invalid Base64 input' : 'Encoding failed');
setError(mode === 'decode' ? t.invalidBase64 : t.encodingFailed);
}
};

Expand All @@ -43,10 +77,10 @@ export default function Base64() {
return (
<div className="space-y-4">
<TextArea
label="Input"
label={t.inputLabel}
value={input}
onChange={e => handleInputChange(e.target.value)}
placeholder="Text to encode, or Base64 to decode"
placeholder={t.placeholder}
monospace={false}
/>

Expand All @@ -56,17 +90,17 @@ export default function Base64() {
aria-pressed={activeMode === 'encode'}
onClick={() => run('encode')}
>
Encode →
{t.encode}
</Button>
<Button
variant={activeMode === 'decode' ? 'primary' : 'secondary'}
aria-pressed={activeMode === 'decode'}
onClick={() => run('decode')}
>
← Decode
{t.decode}
</Button>
<Button variant="ghost" onClick={clear}>
Clear
{t.clear}
</Button>
</div>

Expand All @@ -75,7 +109,7 @@ export default function Base64() {
{output && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">Result</span>
<span className="text-sm font-medium text-muted-foreground">{t.result}</span>
<CopyButton value={output} />
</div>
<pre className="max-h-[20rem] overflow-auto whitespace-pre-wrap break-all rounded-lg border border-border bg-muted/40 p-3 text-sm">
Expand Down
32 changes: 27 additions & 5 deletions src/islands/dev/BaseConvert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,29 @@ import { useState } from 'react';
import { TextArea } from '@/components/ui/TextArea';
import { CopyButton } from '@/components/ui/CopyButton';
import { Alert } from '@/components/ui/Alert';
import type { Lang } from '@/i18n/config';
import { parseInBase } from '@/tools/dev/base-convert.lib';

const TR: Record<Lang, {
value: string;
placeholder: string;
inputBase: string;
invalid: (base: number) => string;
}> = {
en: {
value: 'Value',
placeholder: 'Enter a number',
inputBase: 'Input base',
invalid: (base) => `Not a valid base-${base} number.`,
},
id: {
value: 'Nilai',
placeholder: 'Masukkan angka',
inputBase: 'Basis input',
invalid: (base) => `Bukan angka basis-${base} yang valid.`,
},
};

const INPUT_BASES = [
{ base: 2, label: 'Binary (2)' },
{ base: 8, label: 'Octal (8)' },
Expand All @@ -18,7 +39,8 @@ const OUTPUT_BASES = [
{ base: 16, label: 'Hex' },
];

export default function BaseConvert() {
export default function BaseConvert({ lang = 'en' }: { lang?: Lang }) {
const t = TR[lang] ?? TR.en;
const [input, setInput] = useState('255');
const [inputBase, setInputBase] = useState(10);

Expand All @@ -30,16 +52,16 @@ export default function BaseConvert() {
<div className="flex flex-wrap items-end gap-4">
<div className="min-w-[12rem] flex-1">
<TextArea
label="Value"
label={t.value}
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Enter a number"
placeholder={t.placeholder}
rows={1}
/>
</div>
<label className="space-y-1.5 text-sm">
<span className="block font-bold uppercase tracking-wide text-muted-foreground">
Input base
{t.inputBase}
</span>
<select
value={inputBase}
Expand All @@ -55,7 +77,7 @@ export default function BaseConvert() {
</label>
</div>

{invalid && <Alert variant="error">Not a valid base-{inputBase} number.</Alert>}
{invalid && <Alert variant="error">{t.invalid(inputBase)}</Alert>}

{parsed !== null && (
<div className="divide-y-2 divide-border border-2 border-border">
Expand Down
31 changes: 25 additions & 6 deletions src/islands/dev/ColorConvert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,27 @@ import { useState } from 'react';
import { CopyButton } from '@/components/ui/CopyButton';
import { Alert } from '@/components/ui/Alert';
import { parseColor, toHex, toHsl } from '@/tools/dev/color.lib';
import type { Lang } from '@/i18n/config';

export default function ColorConvert() {
const TR: Record<Lang, { color: string; pick: string; invalid: string; preview: string; placeholder: string }> = {
en: {
color: 'Color',
pick: 'Pick',
invalid: 'Enter a hex (#rrggbb) or rgb(r, g, b) color.',
preview: 'Color preview',
placeholder: '#7c3aed or rgb(124, 58, 237)',
},
id: {
color: 'Warna',
pick: 'Pilih',
invalid: 'Masukkan warna hex (#rrggbb) atau rgb(r, g, b).',
preview: 'Pratinjau warna',
placeholder: '#7c3aed atau rgb(124, 58, 237)',
},
};

export default function ColorConvert({ lang = 'en' }: { lang?: Lang }) {
const t = TR[lang] ?? TR.en;
const [input, setInput] = useState('#7c3aed');

const rgb = parseColor(input);
Expand All @@ -22,18 +41,18 @@ export default function ColorConvert() {
<div className="flex flex-wrap items-end gap-4">
<label className="min-w-[12rem] flex-1 space-y-1.5">
<span className="block text-sm font-bold uppercase tracking-wide text-muted-foreground">
Color
{t.color}
</span>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="#7c3aed or rgb(124, 58, 237)"
placeholder={t.placeholder}
className="w-full border-2 border-border bg-muted px-3 py-2 font-mono text-sm outline-none focus:shadow-brutal-sm"
/>
</label>
<label className="space-y-1.5">
<span className="block text-sm font-bold uppercase tracking-wide text-muted-foreground">
Pick
{t.pick}
</span>
<input
type="color"
Expand All @@ -44,14 +63,14 @@ export default function ColorConvert() {
</label>
</div>

{invalid && <Alert variant="error">Enter a hex (#rrggbb) or rgb(r, g, b) color.</Alert>}
{invalid && <Alert variant="error">{t.invalid}</Alert>}

{rgb && (
<div className="flex flex-col gap-4 sm:flex-row">
<div
className="h-28 w-full border-2 border-border shadow-brutal sm:w-40"
style={{ backgroundColor: toHex(rgb) }}
aria-label="Color preview"
aria-label={t.preview}
/>
<div className="flex-1 divide-y-2 divide-border border-2 border-border">
{rows.map(([label, value]) => (
Expand Down
Loading
Loading