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
22 changes: 11 additions & 11 deletions src/core/registry/tool.registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,17 @@ export const toolRegistry: ToolDefinition[] = [
route: "/tools/url-encoder",
status: "available",
},
{
id: "jwt-secret",
name: "JWT Secret Generator",
category: "Crypto",
description: "Generate secure HS256, HS384, and HS512 JWT secrets locally.",
keywords: ["jwt", "secret", "hs256", "hs512", "hmac"],
icon: ShieldCheck,
persist: false,
route: "/tools/jwt-secret",
status: "available",
},
{
id: "hash-generator",
name: "Hash Generator",
Expand All @@ -129,17 +140,6 @@ export const toolRegistry: ToolDefinition[] = [
route: "/tools/password-generator",
status: "available",
},
{
id: "jwt-secret",
name: "JWT Secret Generator",
category: "Crypto",
description: "Generate secure HS256, HS384, and HS512 JWT secrets locally.",
keywords: ["jwt", "secret", "hs256", "hs512", "hmac"],
icon: ShieldCheck,
persist: false,
route: "/tools/jwt-secret",
status: "available",
},
{
id: "uuid",
name: "UUID Generator",
Expand Down
130 changes: 94 additions & 36 deletions src/features/case-converter/CaseConverterPage.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,118 @@
import type { JSX } from "react";
import { useMemo, useState } from "react";
import { RotateCcw } from "lucide-react";
import { Copy, Download, RotateCcw, TextCursorInput } from "lucide-react";
import { Button } from "@/shared/ui/button";
import {
ResultRow,
PaneHeader,
ToolSurface,
ToolTextarea,
ToolToolbar,
ToolTitle,
} from "@/shared/components/ToolSurface";
import { convertCases } from "./case-converter.service";

const sampleInput = `Forge developer workstation
markdownPreview renderer
JWT secret generator`;

export function CaseConverterPage(): JSX.Element {
const [input, setInput] = useState("Forge developer workstation");
const [input, setInput] = useState(sampleInput);
const variants = useMemo(() => convertCases(input), [input]);

async function copy(value: string): Promise<void> {
await navigator.clipboard.writeText(value);
}

function download(): void {
const blob = new Blob(
[variants.map((variant) => `${variant.label}: ${variant.value}`).join("\n")],
{ type: "text/plain;charset=utf-8" },
);
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");

anchor.href = url;
anchor.download = "forge-case-conversions.txt";
anchor.click();
URL.revokeObjectURL(url);
}

return (
<ToolSurface>
<ToolToolbar
left={
<>
<ToolTitle eyebrow="Utilities" title="Case Converter" />
<p className="hidden text-[12px] font-medium text-slate-500 md:block">
Convert text into common naming conventions.
</p>
</>
<div className="flex min-w-0 items-center gap-2 text-[12px] font-semibold text-sky-600">
<TextCursorInput aria-hidden="true" className="h-4 w-4" />
{variants.length} case styles
</div>
}
right={
<Button
aria-label="Reset"
onClick={() => setInput("")}
size="sm"
variant="ghost"
>
<RotateCcw aria-hidden="true" className="h-4 w-4" />
Reset
</Button>
<>
<Button
className="h-9 px-3"
onClick={() => void copy(variants.map((item) => item.value).join("\n"))}
size="sm"
variant="secondary"
>
<Copy aria-hidden="true" className="h-4 w-4" />
Copy all
</Button>
<Button onClick={download} size="icon" variant="ghost">
<Download aria-hidden="true" className="h-4 w-4" />
</Button>
<Button onClick={() => setInput("")} size="icon" variant="ghost">
<RotateCcw aria-hidden="true" className="h-4 w-4" />
</Button>
</>
}
/>
<div className="grid min-h-0 flex-1 overflow-hidden lg:grid-cols-2">
<ToolTextarea
label="Case input"
onChange={setInput}
placeholder="Text to convert..."
value={input}
/>
<div className="scrollbar-forge min-h-0 overflow-auto border-t border-slate-200 bg-slate-50/50 p-4 lg:border-l lg:border-t-0">
<div className="grid gap-2">
{variants.map((variant) => (
<ResultRow
key={variant.label}
label={variant.label}
onCopy={() => void navigator.clipboard.writeText(variant.value)}
value={variant.value}
/>
))}
<div className="grid min-h-0 flex-1 overflow-hidden xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]">
<div className="flex min-h-0 flex-col">
<PaneHeader
actions={
<span className="text-[12px] font-semibold normal-case tracking-normal text-sky-600">
{input.length.toLocaleString()} chars
</span>
}
title="Input"
tone="blue"
/>
<ToolTextarea
className="selection:bg-sky-200/80 selection:text-slate-950"
label="Case input"
onChange={setInput}
placeholder="Text to convert..."
value={input}
/>
</div>
<div className="flex min-h-0 flex-col border-t border-slate-200 xl:border-l xl:border-t-0">
<PaneHeader title="Converted cases" tone="emerald" />
<div className="scrollbar-forge min-h-0 flex-1 overflow-auto bg-white p-4">
<div className="grid gap-3">
{variants.map((variant) => (
<section
className="rounded-lg border border-slate-200 bg-slate-50 p-3"
key={variant.label}
>
<div className="flex items-center justify-between gap-3">
<p className="text-[13px] font-bold text-slate-950">
{variant.label}
</p>
<Button
className="shrink-0"
onClick={() => void copy(variant.value)}
size="sm"
variant="secondary"
>
<Copy aria-hidden="true" className="h-4 w-4" />
Copy
</Button>
</div>
<p className="mt-3 break-all font-mono text-[13px] leading-6 text-slate-950">
{variant.value || "-"}
</p>
</section>
))}
</div>
</div>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions src/features/case-converter/case-converter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ export function hasCaseConverterInput(input: string): boolean {

export function convertCases(input: string): CaseVariant[] {
const words = splitWords(input);
const lower = words.join(" ");

return [
{ label: "camelCase", value: toCamelCase(words) },
{ label: "PascalCase", value: toPascalCase(words) },
{ label: "snake_case", value: words.join("_") },
{ label: "kebab-case", value: words.join("-") },
{ label: "dot.case", value: words.join(".") },
{ label: "path/case", value: words.join("/") },
{ label: "CONSTANT_CASE", value: words.join("_").toUpperCase() },
{ label: "lowercase", value: lower },
{ label: "UPPERCASE", value: lower.toUpperCase() },
{ label: "Title Case", value: words.map(capitalize).join(" ") },
{ label: "Sentence case", value: toSentenceCase(words) },
];
Expand Down
38 changes: 32 additions & 6 deletions src/features/password-generator/PasswordGeneratorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
RefreshCw,
SlidersHorizontal,
TextCursorInput,
Eye,
EyeOff,
} from "lucide-react";
import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
Expand Down Expand Up @@ -410,17 +412,33 @@ function SecretOutput({
onCopy: () => void;
result: GeneratedSecret;
}): JSX.Element {
const [visible, setVisible] = useState(false);

return (
<div className="scrollbar-forge min-h-0 flex-1 overflow-auto bg-white p-4">
<div className="rounded-lg border border-emerald-100 bg-emerald-50/50 p-4">
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start">
<p className="min-w-0 flex-1 break-all font-mono text-[18px] font-semibold leading-8 text-slate-950">
{result.value}
{visible ? result.value : maskSecret(result.value)}
</p>
<Button className="shrink-0" onClick={onCopy} size="sm" variant="secondary">
<Copy aria-hidden="true" className="h-4 w-4" />
Copy
</Button>
<div className="flex shrink-0 flex-wrap gap-2">
<Button
onClick={() => setVisible((current) => !current)}
size="sm"
variant="ghost"
>
{visible ? (
<EyeOff aria-hidden="true" className="h-4 w-4" />
) : (
<Eye aria-hidden="true" className="h-4 w-4" />
)}
{visible ? "Hide" : "Show"}
</Button>
<Button onClick={onCopy} size="sm" variant="secondary">
<Copy aria-hidden="true" className="h-4 w-4" />
Copy
</Button>
</div>
</div>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-3">
Expand All @@ -432,6 +450,14 @@ function SecretOutput({
);
}

function maskSecret(secret: string): string {
if (secret.length <= 12) {
return "*".repeat(secret.length);
}

return `${secret.slice(0, 6)}${"*".repeat(Math.max(12, secret.length - 12))}${secret.slice(-6)}`;
}

function Metric({ label, value }: { label: string; value: string }): JSX.Element {
return (
<div className="rounded-lg border border-slate-200 bg-white p-3">
Expand Down
Loading
Loading