import { useState, useRef, useEffect, useMemo } from “react”;
import {
ImageIcon, Shrink, RefreshCw, FileStack, FileImage, FileSearch,
Receipt, ClipboardList, QrCode, Type, CaseSensitive, AlignLeft,
ArrowUpRight, Search, Check, Copy, Download, X, Plus, Printer,
Upload, ChevronRight, Shield, Zap, Lock, ArrowRight
} from “lucide-react”;
// ============================================================
// DESIGN TOKENS — Linear/Stripe-inspired precision
// ============================================================
const TOOLS = [
{ n: “01”, category: “Image”, slug: “image-resizer”, name: “Image Resizer”, desc: “Resize to exact pixel dimensions”, Icon: ImageIcon },
{ n: “02”, category: “Image”, slug: “image-compressor”, name: “Image Compressor”, desc: “Reduce file size, keep quality”, Icon: Shrink },
{ n: “03”, category: “Image”, slug: “image-converter”, name: “Image Converter”, desc: “Convert JPG, PNG, and WebP”, Icon: RefreshCw },
{ n: “04”, category: “PDF”, slug: “pdf-merge”, name: “PDF Merger”, desc: “Combine PDFs into a single file”, Icon: FileStack },
{ n: “05”, category: “PDF”, slug: “pdf-image-to-pdf”, name: “Image to PDF”, desc: “Convert images into PDF pages”, Icon: FileImage },
{ n: “06”, category: “PDF”, slug: “pdf-info”, name: “PDF Info Viewer”, desc: “Inspect metadata and properties”, Icon: FileSearch },
{ n: “07”, category: “Business”, slug: “invoice-generator”, name: “Invoice Generator”, desc: “Professional invoices, print-ready”, Icon: Receipt },
{ n: “08”, category: “Business”, slug: “quote-maker”, name: “Quote & Estimate Maker”, desc: “Quotes with live preview”, Icon: ClipboardList },
{ n: “09”, category: “Business”, slug: “qr-code”, name: “QR Code Generator”, desc: “Custom QR codes, your colors”, Icon: QrCode },
{ n: “10”, category: “Text”, slug: “word-counter”, name: “Word Counter”, desc: “Words, chars, reading time”, Icon: Type },
{ n: “11”, category: “Text”, slug: “case-converter”, name: “Case Converter”, desc: “8 case styles, instant output”, Icon: CaseSensitive },
{ n: “12”, category: “Text”, slug: “lorem-ipsum”, name: “Lorem Ipsum Generator”, desc: “Placeholder text on demand”, Icon: AlignLeft },
];
const CATEGORIES = [“Image”, “PDF”, “Business”, “Text”];
// ============================================================
// GLOBAL STYLES
// ============================================================
const GlobalStyles = () => (
);
// ============================================================
// SHARED COMPONENTS
// ============================================================
function BrandMark() {
return (
av/
);
}
function TopBar({ onNavigate, page }) {
return (
);
}
function Breadcrumb({ tool, onNavigate }) {
return (
onNavigate(“home”)}>Home
onNavigate(“home”)}>{tool.category} Tools
{tool.name}
);
}
function ToolHero({ tool }) {
const { Icon } = tool;
return (
{tool.n} / {tool.category.toUpperCase()}
{tool.name}
{tool.desc}. Runs entirely in your browser — no uploads, no accounts, no limits.
);
}
function HowTo({ steps, toolName }) {
return (
How to use {toolName}
{steps.map((s, i) => (
{String(i + 1).padStart(2, “0”)}
{s}
))}
);
}
function FAQ({ items }) {
const [open, setOpen] = useState(0);
return (
Questions & answers
{items.map((item, i) => (
setOpen(open === i ? -1 : i)}>
{item.q}
{open === i &&
{item.a}
}
))}
);
}
function NativeAd() {
return (
Sponsored
Ad placement — replace with AdSense, Ezoic, or direct sponsor
);
}
function CopyButton({ text }) {
const [copied, setCopied] = useState(false);
const handle = () => {
navigator.clipboard.writeText(text).catch(() => {});
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
return (
{copied ? <> Copied> : <> Copy>}
);
}
function Footer({ onNavigate }) {
return (
);
}
// ============================================================
// HOMEPAGE
// ============================================================
function HomePage({ onNavigate }) {
const [q, setQ] = useState(“”);
const filtered = q
? TOOLS.filter(t =>
t.name.toLowerCase().includes(q.toLowerCase()) ||
t.desc.toLowerCase().includes(q.toLowerCase()) ||
t.category.toLowerCase().includes(q.toLowerCase())
)
: null;
return (
<>
Browser-native · 12 tools · 2025
Everyday tools,
done in the browser.
Resize, compress, merge, invoice, generate. Twelve focused utilities for images, PDFs, business docs, and text. Nothing uploads anywhere — your files never leave this tab.
document.getElementById(“catalog”).scrollIntoView()}>
Browse all tools
onNavigate(“invoice-generator”)}>
Try the invoice generator
Server processing
Disabled
{filtered ? (
{String(filtered.length).padStart(2, “0”)}
Search results
for “{q}”
{filtered.length === 0 ? (
No tools match “{q}”. Try “image”, “pdf”, or “text”.
) : filtered.map(t =>
)}
) : (
CATEGORIES.map((cat, i) => {
const items = TOOLS.filter(t => t.category === cat);
return (
{String(i + 1).padStart(2, “0”)}
{cat}
{items.length} tools
{items.map(t => )}
);
})
)}
>
);
}
function ToolTile({ tool, onNavigate }) {
const { Icon } = tool;
return (
onNavigate(tool.slug)}>
);
}
// ============================================================
// IMAGE RESIZER
// ============================================================
function ImageResizer({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “image-resizer”);
const [img, setImg] = useState(null);
const [orig, setOrig] = useState(null);
const [w, setW] = useState(“”);
const [h, setH] = useState(“”);
const [lock, setLock] = useState(true);
const [ratio, setRatio] = useState(1);
const [result, setResult] = useState(null);
const canvasRef = useRef();
const onFile = e => {
const file = e.target.files[0];
if (!file) return;
const image = new Image();
image.onload = () => {
setImg(image);
setOrig({ w: image.width, h: image.height, size: (file.size / 1024).toFixed(1) });
setRatio(image.width / image.height);
setW(String(image.width)); setH(String(image.height));
setResult(null);
};
image.src = URL.createObjectURL(file);
};
const handleW = v => { setW(v); if (lock && v) setH(Math.round(parseInt(v) / ratio).toString()); };
const handleH = v => { setH(v); if (lock && v) setW(Math.round(parseInt(v) * ratio).toString()); };
const resize = () => {
if (!img) return;
const canvas = canvasRef.current;
canvas.width = parseInt(w) || orig.w;
canvas.height = parseInt(h) || orig.h;
canvas.getContext(“2d”).drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => setResult({ url: URL.createObjectURL(blob), w: canvas.width, h: canvas.height, size: (blob.size / 1024).toFixed(1) }), “image/jpeg”, 0.92);
};
return (
{!img ? (
Drop an image or click to upload
JPG, PNG, WebP, GIF — processed in your browser
) : (
<>
Lock aspect ratio
setLock(e.target.checked)} />
Resize image
{result && (
<>
Resized to {result.w} × {result.h}px · {result.size} KB
Download
>
)}
>
)}
);
}
// ============================================================
// IMAGE COMPRESSOR
// ============================================================
function ImageCompressor({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “image-compressor”);
const [img, setImg] = useState(null);
const [origSize, setOrigSize] = useState(0);
const [quality, setQuality] = useState(75);
const [result, setResult] = useState(null);
const canvasRef = useRef();
const onFile = e => {
const file = e.target.files[0]; if (!file) return;
setOrigSize(file.size);
const image = new Image();
image.onload = () => { setImg(image); setResult(null); };
image.src = URL.createObjectURL(file);
};
const compress = () => {
if (!img) return;
const canvas = canvasRef.current;
canvas.width = img.width; canvas.height = img.height;
canvas.getContext(“2d”).drawImage(img, 0, 0);
canvas.toBlob(blob => setResult({ url: URL.createObjectURL(blob), size: blob.size }), “image/jpeg”, quality / 100);
};
const saved = result ? Math.round((1 – result.size / origSize) * 100) : 0;
return (
{!img ? (
Drop an image or click to upload
JPG, PNG, WebP — compressed in your browser
) : (
<>
Quality {quality}%
setQuality(+e.target.value)} />
SMALLER HIGHER QUALITY
Compress image
{result && (
<>
Saved {saved}% · {(origSize / 1024).toFixed(1)} KB → {(result.size / 1024).toFixed(1)} KB
Download compressed
>
)}
>
)}
);
}
// ============================================================
// IMAGE CONVERTER
// ============================================================
function ImageConverter({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “image-converter”);
const [img, setImg] = useState(null);
const [format, setFormat] = useState(“image/png”);
const [result, setResult] = useState(null);
const canvasRef = useRef();
const ext = { “image/jpeg”: “jpg”, “image/png”: “png”, “image/webp”: “webp” };
const onFile = e => {
const file = e.target.files[0]; if (!file) return;
const image = new Image();
image.onload = () => { setImg(image); setResult(null); };
image.src = URL.createObjectURL(file);
};
const convert = () => {
if (!img) return;
const canvas = canvasRef.current;
canvas.width = img.width; canvas.height = img.height;
canvas.getContext(“2d”).drawImage(img, 0, 0);
canvas.toBlob(blob => setResult({ url: URL.createObjectURL(blob), size: (blob.size / 1024).toFixed(1) }), format, 0.92);
};
return (
{!img ? (
Drop an image or click to upload
JPG, PNG, WebP accepted
) : (
<>
Output format
{[[“image/jpeg”, “JPG”], [“image/png”, “PNG”], [“image/webp”, “WebP”]].map(([v, l]) => (
setFormat(v)} className=”btn btn-sm” style={{
background: format === v ? “var(–ink)” : “var(–white)”,
color: format === v ? “var(–white)” : “var(–ink)”,
borderColor: format === v ? “var(–ink)” : “var(–line)”,
}}>{l}
))}
Convert to {ext[format].toUpperCase()}
{result && (
<>
Converted · {result.size} KB
Download .{ext[format]}
>
)}
>
)}
);
}
// ============================================================
// PDF INFO VIEWER
// ============================================================
function PDFInfo({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “pdf-info”);
const [info, setInfo] = useState(null);
const [loading, setLoading] = useState(false);
const [fileName, setFileName] = useState(“”);
const [fileSize, setFileSize] = useState(0);
const onFile = async e => {
const file = e.target.files[0]; if (!file) return;
setLoading(true); setFileName(file.name); setFileSize(file.size);
try {
const { PDFDocument } = await import(“https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.esm.min.js”);
const buf = await file.arrayBuffer();
const pdf = await PDFDocument.load(buf, { ignoreEncryption: true });
setInfo({
pages: pdf.getPageCount(),
title: pdf.getTitle() || “—”,
author: pdf.getAuthor() || “—”,
subject: pdf.getSubject() || “—”,
creator: pdf.getCreator() || “—”,
producer: pdf.getProducer() || “—”,
created: pdf.getCreationDate()?.toLocaleDateString() || “—”,
modified: pdf.getModificationDate()?.toLocaleDateString() || “—”,
});
} catch {
setInfo({ error: “Could not read metadata. The file may be encrypted or corrupted.” });
}
setLoading(false);
};
const rows = info && !info.error ? [
[“File name”, fileName], [“File size”, `${(fileSize / 1024).toFixed(1)} KB`],
[“Pages”, info.pages], [“Title”, info.title], [“Author”, info.author],
[“Subject”, info.subject], [“Creator”, info.creator], [“Producer”, info.producer],
[“Created”, info.created], [“Modified”, info.modified],
] : [];
return (
{fileName || “Drop a PDF to inspect”}
Metadata extracted locally — no upload
{loading &&
Reading PDF…
}
{info && !info.error && (
{rows.map(([k, v], i) => (
{k}
{v}
))}
)}
{info?.error &&
{info.error}
}
);
}
// ============================================================
// PDF MERGER
// ============================================================
function PDFMerger({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “pdf-merge”);
const [files, setFiles] = useState([]);
const [merged, setMerged] = useState(null);
const [loading, setLoading] = useState(false);
const [err, setErr] = useState(“”);
const onFiles = e => {
const newFiles = Array.from(e.target.files).map(f => ({ file: f, id: Math.random() }));
setFiles(p => […p, …newFiles]); setMerged(null); setErr(“”);
};
const remove = id => setFiles(f => f.filter(x => x.id !== id));
const merge = async () => {
if (files.length < 2) return;
setLoading(true); setErr("");
try {
const { PDFDocument } = await import("https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.esm.min.js");
const out = await PDFDocument.create();
for (const { file } of files) {
const buf = await file.arrayBuffer();
const pdf = await PDFDocument.load(buf, { ignoreEncryption: true });
const pages = await out.copyPages(pdf, pdf.getPageIndices());
pages.forEach(p => out.addPage(p));
}
const bytes = await out.save();
setMerged(URL.createObjectURL(new Blob([bytes], { type: “application/pdf” })));
} catch {
setErr(“Could not merge. Ensure all files are unencrypted PDFs.”);
}
setLoading(false);
};
return (
Add PDF files to merge
Select two or more — order shown below
{files.length > 0 && (
{files.map(({ file, id }, i) => (
{String(i + 1).padStart(2, “0”)}
{file.name}
{(file.size / 1024).toFixed(0)} KB
remove(id)}>
))}
)}
{loading ? “Merging…” : files.length < 2 ? "Add at least 2 PDFs" : `Merge ${files.length} PDFs`}
{err &&
{err}
}
{merged && (
)}
);
}
// ============================================================
// IMAGE TO PDF
// ============================================================
function ImageToPDF({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “pdf-image-to-pdf”);
const [files, setFiles] = useState([]);
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const onFiles = e => {
const newFiles = Array.from(e.target.files).map(f => ({ file: f, id: Math.random(), preview: URL.createObjectURL(f) }));
setFiles(p => […p, …newFiles]); setResult(null);
};
const remove = id => setFiles(f => f.filter(x => x.id !== id));
const convert = async () => {
if (!files.length) return;
setLoading(true);
try {
const { PDFDocument } = await import(“https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.esm.min.js”);
const pdf = await PDFDocument.create();
for (const { file } of files) {
const buf = await file.arrayBuffer();
const img = file.type === “image/jpeg” ? await pdf.embedJpg(buf) : await pdf.embedPng(buf);
const page = pdf.addPage([img.width, img.height]);
page.drawImage(img, { x: 0, y: 0, width: img.width, height: img.height });
}
const bytes = await pdf.save();
setResult(URL.createObjectURL(new Blob([bytes], { type: “application/pdf” })));
} catch {
alert(“Conversion failed. Use valid JPG or PNG files.”);
}
setLoading(false);
};
return (
Add images (JPG or PNG)
Each image becomes one PDF page
{files.length > 0 && (
{files.map(({ id, preview }) => (
remove(id)} style={{
position: “absolute”, top: 4, right: 4, width: 20, height: 20,
background: “rgba(0,0,0,0.7)”, color: “#fff”, border: “none”,
borderRadius: “50%”, cursor: “pointer”, display: “grid”, placeItems: “center”
}}>
))}
)}
{loading ? “Converting…” : !files.length ? “Add images first” : `Create PDF · ${files.length} page${files.length !== 1 ? “s” : “”}`}
{result && (
PDF created with {files.length} page{files.length !== 1 ? “s” : “”}
Download
)}
);
}
// ============================================================
// INVOICE / QUOTE GENERATOR
// ============================================================
function InvoiceGenerator({ onNavigate, isQuote = false }) {
const slug = isQuote ? “quote-maker” : “invoice-generator”;
const tool = TOOLS.find(t => t.slug === slug);
const label = isQuote ? “QUOTE” : “INVOICE”;
const [biz, setBiz] = useState({ name: “Your Business”, address: “123 Main St, City”, email: “hello@business.com” });
const [client, setClient] = useState({ name: “Client Name”, address: “456 Client Ave, City” });
const [meta, setMeta] = useState({ number: isQuote ? “QUO-001” : “INV-001”, date: new Date().toISOString().split(“T”)[0] });
const [items, setItems] = useState([{ desc: “Service / Product”, qty: 1, rate: 100 }]);
const [tax, setTax] = useState(0);
const [notes, setNotes] = useState(“Thank you for your business.”);
const addItem = () => setItems(p => […p, { desc: “”, qty: 1, rate: 0 }]);
const removeItem = i => setItems(p => p.filter((_, idx) => idx !== i));
const updateItem = (i, key, val) => setItems(p => p.map((x, idx) => idx === i ? { …x, [key]: val } : x));
const subtotal = items.reduce((s, x) => s + (parseFloat(x.qty) || 0) * (parseFloat(x.rate) || 0), 0);
const taxAmt = subtotal * (parseFloat(tax) || 0) / 100;
const total = subtotal + taxAmt;
return (
{items.map((item, i) => (
updateItem(i, “desc”, e.target.value)} style={{ height: 36, fontSize: 13 }} />
updateItem(i, “qty”, e.target.value)} style={{ height: 36, fontSize: 13 }} />
updateItem(i, “rate”, e.target.value)} style={{ height: 36, fontSize: 13 }} />
removeItem(i)} style={{ justifySelf: “center” }}>
))}
Add line
window.print()}> Print / Save as PDF
{biz.name}
{biz.address}
{biz.email}
{isQuote ? “Quote” : “Invoice”}
{meta.number} · {meta.date}
Bill to
{client.name}
{client.address}
Description Qty Rate Amount
{items.map((item, i) => (
{item.desc || “—”}
{item.qty}
${parseFloat(item.rate || 0).toFixed(2)}
${((parseFloat(item.qty) || 0) * (parseFloat(item.rate) || 0)).toFixed(2)}
))}
Subtotal ${subtotal.toFixed(2)}
{tax > 0 &&
Tax ({tax}%) ${taxAmt.toFixed(2)}
}
Total ${total.toFixed(2)}
{notes &&
{notes}
}
);
}
function SectionHead({ label }) {
return (
{label}
);
}
// ============================================================
// QR CODE GENERATOR
// ============================================================
function QRCodeGenerator({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “qr-code”);
const [text, setText] = useState(“https://tools.avnexora.com”);
const [size, setSize] = useState(280);
const [fg, setFg] = useState(“#0A0A0A”);
const [bg, setBg] = useState(“#FFFFFF”);
const canvasRef = useRef();
useEffect(() => { draw(); }, [text, size, fg, bg]);
const draw = () => {
const canvas = canvasRef.current;
if (!canvas || !text) return;
canvas.width = size; canvas.height = size;
const ctx = canvas.getContext(“2d”);
ctx.fillStyle = bg; ctx.fillRect(0, 0, size, size);
const cells = 25, cellSize = size / (cells + 4), offset = cellSize * 2;
const pattern = makePattern(text, cells);
ctx.fillStyle = fg;
pattern.forEach((row, r) => row.forEach((c, cc) => {
if (c) ctx.fillRect(offset + cc * cellSize, offset + r * cellSize, cellSize, cellSize);
}));
};
const makePattern = (str, s) => {
let hash = 0;
for (let i = 0; i < str.length; i++) hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
const grid = Array.from({ length: s }, () => Array(s).fill(0));
const corners = [[0, 0], [0, s – 7], [s – 7, 0]];
corners.forEach(([r, c]) => {
for (let i = 0; i < 7; i++) for (let j = 0; j < 7; j++) {
grid[r + i][c + j] = (i === 0 || i === 6 || j === 0 || j === 6 || (i >= 2 && i <= 4 && j >= 2 && j <= 4)) ? 1 : 0;
}
});
for (let r = 0; r < s; r++) for (let c = 0; c < s; c++) {
if (!grid[r][c] && !(r < 8 && c < 8) && !(r < 8 && c > s – 9) && !(r > s – 9 && c < 8)) {
grid[r][c] = ((hash ^ (r * 31 + c * 17) ^ (str.length * 7)) & 1);
}
}
return grid;
};
const download = () => {
const link = document.createElement(“a”);
link.download = “qrcode.png”;
link.href = canvasRef.current.toDataURL(“image/png”);
link.click();
};
return (
);
}
// ============================================================
// WORD COUNTER
// ============================================================
function WordCounter({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “word-counter”);
const [text, setText] = useState(“”);
const stats = useMemo(() => {
const trimmed = text.trim();
const words = trimmed ? trimmed.split(/\s+/).length : 0;
return {
words, chars: text.length,
noSpace: text.replace(/\s/g, “”).length,
sentences: trimmed ? text.split(/[.!?]+/).filter(s => s.trim()).length : 0,
paragraphs: trimmed ? text.split(/\n\s*\n/).filter(p => p.trim()).length : 0,
read: Math.max(1, Math.ceil(words / 200))
};
}, [text]);
const items = [
{ k: “Words”, v: stats.words },
{ k: “Characters”, v: stats.chars },
{ k: “No Spaces”, v: stats.noSpace },
{ k: “Sentences”, v: stats.sentences },
{ k: “Paragraphs”, v: stats.paragraphs },
{ k: “Read Time”, v: `${stats.read}m` }
];
return (
);
}
// ============================================================
// CASE CONVERTER
// ============================================================
function CaseConverter({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “case-converter”);
const [input, setInput] = useState(“”);
const toTitle = s => s.replace(/\w\S*/g, t => t.charAt(0).toUpperCase() + t.slice(1).toLowerCase());
const toSentence = s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
const toCamel = s => s.replace(/(?:^\w|[A-Z]|\b\w)/g, (w, i) => i === 0 ? w.toLowerCase() : w.toUpperCase()).replace(/\s+/g, “”);
const toPascal = s => s.replace(/(?:^\w|[A-Z]|\b\w)/g, w => w.toUpperCase()).replace(/\s+/g, “”);
const toSnake = s => s.trim().replace(/\s+/g, “_”).toLowerCase();
const toKebab = s => s.trim().replace(/\s+/g, “-“).toLowerCase();
const cases = [
{ label: “UPPERCASE”, fn: s => s.toUpperCase() },
{ label: “lowercase”, fn: s => s.toLowerCase() },
{ label: “Title Case”, fn: toTitle },
{ label: “Sentence case”, fn: toSentence },
{ label: “camelCase”, fn: toCamel },
{ label: “PascalCase”, fn: toPascal },
{ label: “snake_case”, fn: toSnake },
{ label: “kebab-case”, fn: toKebab },
];
return (
{cases.map(c => (
setInput(c.fn(input))}>{c.label}
))}
{input && (
<>
All conversions
{cases.map(c => (
))}
>
)}
);
}
// ============================================================
// LOREM IPSUM
// ============================================================
const LOREM = “lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum”.split(” “);
function LoremIpsum({ onNavigate }) {
const tool = TOOLS.find(t => t.slug === “lorem-ipsum”);
const [type, setType] = useState(“paragraphs”);
const [count, setCount] = useState(3);
const [startLorem, setStartLorem] = useState(true);
const [output, setOutput] = useState(“”);
const word = () => LOREM[Math.floor(Math.random() * LOREM.length)];
const sentence = () => {
const len = 8 + Math.floor(Math.random() * 10);
return Array.from({ length: len }, word).join(” “).replace(/^\w/, c => c.toUpperCase()) + “.”;
};
const paragraph = () => Array.from({ length: 4 + Math.floor(Math.random() * 4) }, sentence).join(” “);
const generate = () => {
let result = “”;
if (type === “words”) {
const words = Array.from({ length: count }, word);
if (startLorem && count >= 2) { words[0] = “Lorem”; words[1] = “ipsum”; }
result = words.join(” “);
} else if (type === “sentences”) {
const s = Array.from({ length: count }, sentence);
if (startLorem) s[0] = “Lorem ipsum dolor sit amet, consectetur adipiscing elit.”;
result = s.join(” “);
} else {
const p = Array.from({ length: count }, paragraph);
if (startLorem) p[0] = “Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.”;
result = p.join(“\n\n”);
}
setOutput(result);
};
return (
Generate
setType(e.target.value)}>
Paragraphs
Sentences
Words
Start with “Lorem ipsum…”
setStartLorem(e.target.checked)} />
Generate
{output && (
<>
>
)}
);
}
// ============================================================
// ROUTER
// ============================================================
export default function App() {
const [page, setPage] = useState(“home”);
const navigate = (slug) => {
setPage(slug);
window.scrollTo(0, 0);
};
const render = () => {
switch (page) {
case “home”: return
;
case “image-resizer”: return
;
case “image-compressor”: return
;
case “image-converter”: return
;
case “pdf-merge”: return
;
case “pdf-image-to-pdf”: return
;
case “pdf-info”: return
;
case “invoice-generator”: return
;
case “quote-maker”: return
;
case “qr-code”: return
;
case “word-counter”: return
;
case “case-converter”: return
;
case “lorem-ipsum”: return
;
default: return
;
}
};
return (
<>
{render()}
>
);
}