From 35cf179869b0508db40e4b3b3172bf13b16c8a11 Mon Sep 17 00:00:00 2001 From: Kresna <13603341+slaveofcode@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:29:59 +0700 Subject: [PATCH] feat(dev): add 5 developer reference tools Slugify, HTTP status code lookup, MIME type lookup, Lorem Ipsum generator, and a 'What is my browser' info tool. Each has a pure, unit-tested lib and a thin island, plus EN + ID SEO entries. --- src/islands/dev/BrowserInfo.tsx | 116 +++++++++++++++++ src/islands/dev/HttpStatus.tsx | 63 ++++++++++ src/islands/dev/LoremIpsum.tsx | 93 ++++++++++++++ src/islands/dev/MimeLookup.tsx | 58 +++++++++ src/islands/dev/Slugify.tsx | 80 ++++++++++++ src/registry/tool-seo.ts | 166 +++++++++++++++++++++++++ src/registry/tools.ts | 57 ++++++++- src/tools/dev/browser-info.lib.test.ts | 63 ++++++++++ src/tools/dev/browser-info.lib.ts | 61 +++++++++ src/tools/dev/http-status.lib.test.ts | 47 +++++++ src/tools/dev/http-status.lib.ts | 122 ++++++++++++++++++ src/tools/dev/lorem.lib.test.ts | 43 +++++++ src/tools/dev/lorem.lib.ts | 93 ++++++++++++++ src/tools/dev/mime.lib.test.ts | 39 ++++++ src/tools/dev/mime.lib.ts | 113 +++++++++++++++++ src/tools/dev/slugify.lib.test.ts | 37 ++++++ src/tools/dev/slugify.lib.ts | 27 ++++ 17 files changed, 1277 insertions(+), 1 deletion(-) create mode 100644 src/islands/dev/BrowserInfo.tsx create mode 100644 src/islands/dev/HttpStatus.tsx create mode 100644 src/islands/dev/LoremIpsum.tsx create mode 100644 src/islands/dev/MimeLookup.tsx create mode 100644 src/islands/dev/Slugify.tsx create mode 100644 src/tools/dev/browser-info.lib.test.ts create mode 100644 src/tools/dev/browser-info.lib.ts create mode 100644 src/tools/dev/http-status.lib.test.ts create mode 100644 src/tools/dev/http-status.lib.ts create mode 100644 src/tools/dev/lorem.lib.test.ts create mode 100644 src/tools/dev/lorem.lib.ts create mode 100644 src/tools/dev/mime.lib.test.ts create mode 100644 src/tools/dev/mime.lib.ts create mode 100644 src/tools/dev/slugify.lib.test.ts create mode 100644 src/tools/dev/slugify.lib.ts diff --git a/src/islands/dev/BrowserInfo.tsx b/src/islands/dev/BrowserInfo.tsx new file mode 100644 index 0000000..7f6cf67 --- /dev/null +++ b/src/islands/dev/BrowserInfo.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react'; +import { CopyButton } from '@/components/ui/CopyButton'; +import { parseUserAgent } from '@/tools/dev/browser-info.lib'; +import type { Lang } from '@/i18n/config'; + +const TR: Record = { + en: { + intro: 'See what your browser reports about itself — user agent, browser & OS, screen and viewport size, timezone and more. Nothing is uploaded; this reads only what your own browser exposes to any web page.', + copyAll: 'Copy all as JSON', + groups: { browser: 'Browser', display: 'Screen & viewport', device: 'Device', locale: 'Locale & network' }, + }, + id: { + intro: 'Lihat apa yang dilaporkan browser Anda tentang dirinya — user agent, browser & OS, ukuran layar dan viewport, zona waktu, dan lainnya. Tidak ada yang diunggah; ini hanya membaca yang diekspos browser Anda ke halaman web mana pun.', + copyAll: 'Salin semua sebagai JSON', + groups: { browser: 'Browser', display: 'Layar & viewport', device: 'Perangkat', locale: 'Bahasa & jaringan' }, + }, +}; + +type Row = { label: string; value: string }; +type Info = { browser: Row[]; display: Row[]; device: Row[]; locale: Row[]; all: Record }; + +// navigator.deviceMemory is non-standard and not in the TS lib types. +interface ExtraNavigator extends Navigator { + deviceMemory?: number; +} + +function collect(): Info { + const nav = navigator as ExtraNavigator; + const ua = nav.userAgent; + const parsed = parseUserAgent(ua); + const dpr = window.devicePixelRatio || 1; + const tz = Intl.DateTimeFormat().resolvedOptions().timeZone || '—'; + const langs = nav.languages && nav.languages.length ? nav.languages.join(', ') : nav.language || '—'; + + const browser: Row[] = [ + { label: 'User agent', value: ua }, + { label: 'Browser', value: `${parsed.browser}${parsed.browserVersion ? ' ' + parsed.browserVersion : ''}` }, + { label: 'Engine', value: parsed.engine }, + { label: 'Operating system', value: parsed.os }, + ]; + const display: Row[] = [ + { label: 'Screen resolution', value: `${screen.width} × ${screen.height}` }, + { label: 'Available screen', value: `${screen.availWidth} × ${screen.availHeight}` }, + { label: 'Viewport (window)', value: `${window.innerWidth} × ${window.innerHeight}` }, + { label: 'Device pixel ratio', value: String(dpr) }, + { label: 'Color depth', value: `${screen.colorDepth}-bit` }, + { label: 'Orientation', value: screen.orientation?.type ?? '—' }, + ]; + const device: Row[] = [ + { label: 'CPU cores (logical)', value: nav.hardwareConcurrency ? String(nav.hardwareConcurrency) : '—' }, + { label: 'Device memory', value: nav.deviceMemory ? `${nav.deviceMemory} GB (approx.)` : '—' }, + { label: 'Touch points', value: String(nav.maxTouchPoints ?? 0) }, + { label: 'Platform', value: nav.platform || '—' }, + ]; + const locale: Row[] = [ + { label: 'Languages', value: langs }, + { label: 'Timezone', value: tz }, + { label: 'Cookies enabled', value: nav.cookieEnabled ? 'Yes' : 'No' }, + { label: 'Online', value: nav.onLine ? 'Yes' : 'No' }, + { label: 'Do Not Track', value: nav.doNotTrack === '1' ? 'Enabled' : 'Not enabled' }, + ]; + + const all: Record = {}; + for (const r of [...browser, ...display, ...device, ...locale]) all[r.label] = r.value; + return { browser, display, device, locale, all }; +} + +function Section({ title, rows }: { title: string; rows: Row[] }) { + return ( +
+

{title}

+
    + {rows.map(r => ( +
  • + {r.label} + {r.value} + +
  • + ))} +
+
+ ); +} + +export default function BrowserInfo({ lang = 'en' }: { lang?: Lang }) { + const t = TR[lang] ?? TR.en; + const [info, setInfo] = useState(null); + + useEffect(() => { + const update = () => setInfo(collect()); + update(); + window.addEventListener('resize', update); + window.addEventListener('online', update); + window.addEventListener('offline', update); + return () => { + window.removeEventListener('resize', update); + window.removeEventListener('online', update); + window.removeEventListener('offline', update); + }; + }, []); + + if (!info) return

{t.intro}

; + + return ( +
+
+

{t.intro}

+ +
+
+
+
+
+
+ ); +} diff --git a/src/islands/dev/HttpStatus.tsx b/src/islands/dev/HttpStatus.tsx new file mode 100644 index 0000000..dbd7638 --- /dev/null +++ b/src/islands/dev/HttpStatus.tsx @@ -0,0 +1,63 @@ +import { useMemo, useState } from 'react'; +import { searchStatuses, CLASS_LABELS, type StatusClass } from '@/tools/dev/http-status.lib'; +import type { Lang } from '@/i18n/config'; + +const TR: Record = { + en: { + intro: 'Look up any HTTP status code — search by number (e.g. 422), class (e.g. 4xx) or name (e.g. "gateway"). Everything runs in your browser.', + placeholder: 'Search a code, class or name — e.g. 404, 5xx, timeout', + empty: 'No status codes match your search.', + }, + id: { + intro: 'Cari kode status HTTP mana pun — berdasarkan nomor (mis. 422), kelas (mis. 4xx), atau nama (mis. "gateway"). Semuanya berjalan di browser Anda.', + placeholder: 'Cari kode, kelas, atau nama — mis. 404, 5xx, timeout', + empty: 'Tidak ada kode status yang cocok.', + }, +}; + +const CLASS_STYLE: Record = { + '1xx': 'bg-sky-500/15 text-sky-600 dark:text-sky-400', + '2xx': 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400', + '3xx': 'bg-amber-500/15 text-amber-600 dark:text-amber-400', + '4xx': 'bg-orange-500/15 text-orange-600 dark:text-orange-400', + '5xx': 'bg-red-500/15 text-red-600 dark:text-red-400', +}; + +export default function HttpStatus({ lang = 'en' }: { lang?: Lang }) { + const t = TR[lang] ?? TR.en; + const [query, setQuery] = useState(''); + const results = useMemo(() => searchStatuses(query), [query]); + + return ( +
+

{t.intro}

+ + setQuery(e.target.value)} + placeholder={t.placeholder} + className="w-full rounded-lg border border-border bg-muted/40 px-3 py-2 text-sm outline-none focus:border-accent" + /> + + {results.length === 0 ? ( +

{t.empty}

+ ) : ( +
    + {results.map(s => ( +
  • +
    + {s.code} + {s.name} + + {CLASS_LABELS[s.category]} + +
    +

    {s.description}

    +
  • + ))} +
+ )} +
+ ); +} diff --git a/src/islands/dev/LoremIpsum.tsx b/src/islands/dev/LoremIpsum.tsx new file mode 100644 index 0000000..dc0cfb0 --- /dev/null +++ b/src/islands/dev/LoremIpsum.tsx @@ -0,0 +1,93 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/Button'; +import { TextArea } from '@/components/ui/TextArea'; +import { CopyButton } from '@/components/ui/CopyButton'; +import { generateLorem, type LoremUnit } from '@/tools/dev/lorem.lib'; +import type { Lang } from '@/i18n/config'; + +const TR: Record; +}> = { + en: { + intro: 'Generate placeholder Lorem Ipsum text — by paragraphs, sentences or words — for mockups and layouts. Runs entirely in your browser.', + amount: 'Amount', + unit: 'Type', + startWith: 'Start with "Lorem ipsum…"', + generate: 'Generate', + output: 'Result', + units: { paragraphs: 'Paragraphs', sentences: 'Sentences', words: 'Words' }, + }, + id: { + intro: 'Buat teks placeholder Lorem Ipsum — per paragraf, kalimat, atau kata — untuk mockup dan tata letak. Berjalan sepenuhnya di browser Anda.', + amount: 'Jumlah', + unit: 'Tipe', + startWith: 'Mulai dengan "Lorem ipsum…"', + generate: 'Buat', + output: 'Hasil', + units: { paragraphs: 'Paragraf', sentences: 'Kalimat', words: 'Kata' }, + }, +}; + +const UNITS: LoremUnit[] = ['paragraphs', 'sentences', 'words']; + +export default function LoremIpsum({ lang = 'en' }: { lang?: Lang }) { + const t = TR[lang] ?? TR.en; + const [unit, setUnit] = useState('paragraphs'); + const [count, setCount] = useState(3); + const [startWithLorem, setStartWithLorem] = useState(true); + const [seed, setSeed] = useState(1); + const [output, setOutput] = useState(''); + + const generate = () => { + const nextSeed = seed + 1; + setSeed(nextSeed); + const safe = Math.min(Math.max(count, 1), 1000); + setOutput(generateLorem({ unit, count: safe, startWithLorem, seed: nextSeed })); + }; + + return ( +
+

{t.intro}

+ +
+ + + + +
+ + {output && ( +
+
+ {t.output} + +
+