From 69412ed8d5ec7a0a88743c4e6cab63a11d694a02 Mon Sep 17 00:00:00 2001 From: ajithakdev Date: Mon, 7 Sep 2026 23:59:40 +0530 Subject: [PATCH 1/3] feat(case-converter): add text case converter and slug generator (closes #76) --- src/pages/Landing.tsx | 2 +- .../case-converter/CaseConverterTool.tsx | 300 ++++++++++++++++++ .../case-converter/caseConverter.test.ts | 98 ++++++ src/tools/case-converter/caseConverter.ts | 124 ++++++++ .../case-converter/caseConverterTool.test.tsx | 133 ++++++++ src/tools/registry.tsx | 12 +- 6 files changed, 667 insertions(+), 2 deletions(-) create mode 100644 src/tools/case-converter/CaseConverterTool.tsx create mode 100644 src/tools/case-converter/caseConverter.test.ts create mode 100644 src/tools/case-converter/caseConverter.ts create mode 100644 src/tools/case-converter/caseConverterTool.test.tsx diff --git a/src/pages/Landing.tsx b/src/pages/Landing.tsx index 265bd0c..04f27d6 100644 --- a/src/pages/Landing.tsx +++ b/src/pages/Landing.tsx @@ -9,7 +9,7 @@ import { Clock, Filter, Grid2X2 } from 'lucide-react'; const CATEGORIES = [ { name: 'Generators', slugs: ['uuid', 'nanoid', 'password', 'objectid', 'qr'] }, { name: 'Encoders & Decoders', slugs: ['base64', 'url', 'jwt-decode'] }, - { name: 'Converters & Formatters', slugs: ['json', 'color', 'json-to-ts'] }, + { name: 'Converters & Formatters', slugs: ['json', 'color', 'json-to-ts', 'case-converter'] }, { name: 'API & Networking', slugs: ['api-tester'] }, { name: 'Security & Crypto', slugs: ['hash', 'jwt'] }, { name: 'Utilities', slugs: ['timestamp', 'regex', 'markdown'] } diff --git a/src/tools/case-converter/CaseConverterTool.tsx b/src/tools/case-converter/CaseConverterTool.tsx new file mode 100644 index 0000000..e51800c --- /dev/null +++ b/src/tools/case-converter/CaseConverterTool.tsx @@ -0,0 +1,300 @@ +import { useState, useMemo, useEffect } from 'react'; +import { ToolLayout } from '../../components/ToolLayout'; +import { Button } from '../../components/ui/Button'; +import { CopyButton } from '../../components/ui/CopyButton'; +import { useUrlState } from '../../hooks/useUrlState'; +import { + convertAllCases, + detectCasing, + extractWords, + toCamelCase, + toPascalCase, + toSnakeCase, + toConstantCase, + toKebabCase, + toSlug, + toTitleCase, + toSentenceCase, + toDotCase, + toPathCase, + type CaseResult, +} from './caseConverter'; + +const SAMPLES = [ + 'user_profile_data', + 'APIResponseHandler', + 'fast-furious-tokyo-drift', + 'Café & Restaurant', + 'document.body.style', +]; + +const BATCH_TARGETS = [ + { label: 'camelCase', fn: (w: string[]) => toCamelCase(w) }, + { label: 'PascalCase', fn: (w: string[]) => toPascalCase(w) }, + { label: 'snake_case', fn: (w: string[]) => toSnakeCase(w) }, + { label: 'CONSTANT_CASE', fn: (w: string[]) => toConstantCase(w) }, + { label: 'kebab-case', fn: (w: string[]) => toKebabCase(w) }, + { label: 'URL Slug', fn: (_w: string[], raw: string) => toSlug(raw) }, + { label: 'Title Case', fn: (w: string[]) => toTitleCase(w) }, + { label: 'Sentence case', fn: (w: string[]) => toSentenceCase(w) }, + { label: 'dot.case', fn: (w: string[]) => toDotCase(w) }, + { label: 'path/case', fn: (w: string[]) => toPathCase(w) }, + { label: 'lowercase', fn: (_w: string[], raw: string) => raw.toLowerCase() }, + { label: 'UPPERCASE', fn: (_w: string[], raw: string) => raw.toUpperCase() }, +]; + +export default function CaseConverterTool() { + const [urlText, setUrlText] = useUrlState('t', 'hello_world_example', String, String); + const [text, setTextInternal] = useState(urlText); + const [isBatch, setIsBatch] = useState(false); + const [batchTarget, setBatchTarget] = useState('camelCase'); + + const setText = (val: string) => { + setTextInternal(val); + setUrlText(val); + }; + + useEffect(() => { + setTextInternal(urlText); + }, [urlText]); + + const detected = useMemo(() => detectCasing(text), [text]); + const results: CaseResult[] = useMemo(() => convertAllCases(text), [text]); + + const batchOutput = useMemo(() => { + if (!text) return ''; + const targetObj = BATCH_TARGETS.find((t) => t.label === batchTarget) || BATCH_TARGETS[0]; + return text + .split('\n') + .map((line) => { + const words = extractWords(line); + return targetObj.fn(words, line); + }) + .join('\n'); + }, [text, batchTarget]); + + return ( + +
+ {/* Controls row */} +
+ {/* Quick samples */} +
+ Examples: + {SAMPLES.map((s) => ( + + ))} +
+ + {/* Mode Switcher */} +
+ + +
+
+ + {/* Input Textarea */} +
+