diff --git a/app/(app)/random/page.tsx b/app/(app)/random/page.tsx new file mode 100644 index 0000000..a75572c --- /dev/null +++ b/app/(app)/random/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from 'next'; +import { RandomGenerator } from '@/components/random/RandomGenerator'; +import { AppPage } from '@/components/layout/AppPage'; +import { getToolByHref } from '@/lib/tools'; + +const tool = getToolByHref('/random')!; + +export const metadata: Metadata = { title: tool.title, description: tool.summary }; + +export default function RandomPage() { + return ( + + + + ); +} diff --git a/components/AppIcons.tsx b/components/AppIcons.tsx index 2fa5b90..c8a1551 100644 --- a/components/AppIcons.tsx +++ b/components/AppIcons.tsx @@ -67,6 +67,17 @@ export const QRCodeIcon = (props: React.SVGProps) => ( ); +export const RandomIcon = (props: React.SVGProps) => ( + + + + + + + + +); + export const CalculateIcon = (props: React.SVGProps) => ( {/* Y-axis */} diff --git a/components/random/RandomGenerator.tsx b/components/random/RandomGenerator.tsx new file mode 100644 index 0000000..4a3ea5a --- /dev/null +++ b/components/random/RandomGenerator.tsx @@ -0,0 +1,436 @@ +'use client'; + +import { useState, useCallback } from 'react'; +import { RefreshCw, Copy, Check, Clock } from 'lucide-react'; +import { toast } from 'sonner'; +import { cn } from '@/lib/utils/cn'; +import { SliderRow } from '@/components/ui/slider-row'; +import { MobileTabs } from '@/components/ui/mobile-tabs'; +import { + generatePassword, passwordEntropy, + generateUUID, + generateApiKey, + generateHash, + generateToken, + type PasswordOpts, + type ApiKeyOpts, + type HashOpts, + type TokenOpts, +} from '@/lib/random/generators'; + +type GeneratorType = 'password' | 'uuid' | 'apikey' | 'hash' | 'token'; +type MobileTab = 'configure' | 'output'; + +const GENERATOR_TABS: { value: GeneratorType; label: string }[] = [ + { value: 'password', label: 'Password' }, + { value: 'uuid', label: 'UUID' }, + { value: 'apikey', label: 'API Key' }, + { value: 'hash', label: 'Hash' }, + { value: 'token', label: 'Token' }, +]; + +const selectCls = + 'w-full bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 cursor-pointer'; + +const strengthLabel = (bits: number) => { + if (bits < 40) return { label: 'Weak', color: 'bg-red-500' }; + if (bits < 60) return { label: 'Fair', color: 'bg-amber-400' }; + if (bits < 80) return { label: 'Good', color: 'bg-yellow-400' }; + if (bits < 100) return { label: 'Strong', color: 'bg-emerald-400' }; + return { label: 'Very Strong', color: 'bg-primary' }; +}; + +export function RandomGenerator() { + const [type, setType] = useState('password'); + const [mobileTab, setMobileTab] = useState('configure'); + const [output, setOutput] = useState(''); + const [copied, setCopied] = useState(false); + const [generating, setGenerating] = useState(false); + const [history, setHistory] = useState([]); + + // Options per type + const [pwOpts, setPwOpts] = useState({ + length: 24, uppercase: true, lowercase: true, numbers: true, symbols: true, + }); + const [apiOpts, setApiOpts] = useState({ + length: 32, format: 'hex', prefix: '', + }); + const [hashOpts, setHashOpts] = useState({ + algorithm: 'SHA-256', input: '', + }); + const [tokenOpts, setTokenOpts] = useState({ + bytes: 32, format: 'hex', + }); + + const pushHistory = (val: string) => + setHistory((h) => [val, ...h].slice(0, 8)); + + const generate = useCallback(async () => { + setGenerating(true); + try { + let result = ''; + switch (type) { + case 'password': result = generatePassword(pwOpts); break; + case 'uuid': result = generateUUID(); break; + case 'apikey': result = generateApiKey(apiOpts); break; + case 'hash': result = await generateHash(hashOpts); break; + case 'token': result = generateToken(tokenOpts); break; + } + setOutput(result); + pushHistory(result); + setMobileTab('output'); + } catch { + toast.error('Generation failed'); + } finally { + setGenerating(false); + } + }, [type, pwOpts, apiOpts, hashOpts, tokenOpts]); + + const copy = (val = output) => { + if (!val) return; + navigator.clipboard.writeText(val); + setCopied(true); + toast.success('Copied to clipboard'); + setTimeout(() => setCopied(false), 2000); + }; + + const entropy = type === 'password' ? passwordEntropy(pwOpts) : null; + const strength = entropy !== null ? strengthLabel(entropy) : null; + + return ( +
+ + setMobileTab(v as MobileTab)} + /> + +
+ {/* ── Left: type selector + options ───────────────────── */} +
+ {/* Type selector */} +
+ + Generator + +
+ {GENERATOR_TABS.map(({ value, label }) => ( + + ))} +
+
+ + {/* Options */} +
+ + Options + + + {/* ── Password ── */} + {type === 'password' && ( +
+ setPwOpts((o) => ({ ...o, length: v }))} + /> +
+ + Character sets + +
+ {([ + { key: 'uppercase', label: 'A–Z', hint: 'Uppercase' }, + { key: 'lowercase', label: 'a–z', hint: 'Lowercase' }, + { key: 'numbers', label: '0–9', hint: 'Numbers' }, + { key: 'symbols', label: '!@#', hint: 'Symbols' }, + ] as const).map(({ key, label, hint }) => ( + + ))} +
+
+ {strength && ( +
+
+ + Strength + + + {entropy} bits + +
+
+
+
+ + {strength.label} + +
+ )} +
+ )} + + {/* ── UUID ── */} + {type === 'uuid' && ( +
+
+

+ Generates a cryptographically random UUID v4 using the browser's built-in{' '} + crypto.randomUUID(). +

+
+

+ Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx +

+
+ )} + + {/* ── API Key ── */} + {type === 'apikey' && ( +
+ setApiOpts((o) => ({ ...o, length: v }))} + /> +
+ + Encoding + + +
+
+ + Prefix (optional) + + setApiOpts((o) => ({ ...o, prefix: e.target.value }))} + placeholder="sk, pk, api..." + className="w-full bg-transparent border border-border/40 rounded-lg px-2.5 py-1.5 text-xs font-mono outline-none focus:border-primary/50 transition-colors text-foreground/80 placeholder:text-muted-foreground/25" + /> +
+
+ )} + + {/* ── Hash ── */} + {type === 'hash' && ( +
+
+ + Algorithm + + +
+
+ + Input (empty = random) + +