feat(mini-utilities): add 5 new tools - U/R converter, AI cleaner, MDLPA, PDF reducer, OCR
This commit is contained in:
@@ -1,13 +1,35 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Copy, Check, Hash, Type, Percent, Ruler } from 'lucide-react';
|
||||
import { Button } from '@/shared/components/ui/button';
|
||||
import { Input } from '@/shared/components/ui/input';
|
||||
import { Label } from '@/shared/components/ui/label';
|
||||
import { Textarea } from '@/shared/components/ui/textarea';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components/ui/tabs';
|
||||
import { useState, useRef } from "react";
|
||||
import {
|
||||
Copy,
|
||||
Check,
|
||||
Hash,
|
||||
Type,
|
||||
Percent,
|
||||
Ruler,
|
||||
Zap,
|
||||
Wand2,
|
||||
Building2,
|
||||
FileDown,
|
||||
ScanText,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { Textarea } from "@/shared/components/ui/textarea";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/shared/components/ui/tabs";
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -16,17 +38,29 @@ function CopyButton({ text }: { text: string }) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch { /* silent */ }
|
||||
} catch {
|
||||
/* silent */
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCopy} disabled={!text}>
|
||||
{copied ? <Check className="h-3.5 w-3.5 text-green-500" /> : <Copy className="h-3.5 w-3.5" />}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7"
|
||||
onClick={handleCopy}
|
||||
disabled={!text}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function TextCaseConverter() {
|
||||
const [input, setInput] = useState('');
|
||||
const [input, setInput] = useState("");
|
||||
const upper = input.toUpperCase();
|
||||
const lower = input.toLowerCase();
|
||||
const title = input.replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
@@ -34,15 +68,26 @@ function TextCaseConverter() {
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div><Label>Text sursă</Label><Textarea value={input} onChange={(e) => setInput(e.target.value)} rows={3} className="mt-1" placeholder="Introdu text..." /></div>
|
||||
<div>
|
||||
<Label>Text sursă</Label>
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
rows={3}
|
||||
className="mt-1"
|
||||
placeholder="Introdu text..."
|
||||
/>
|
||||
</div>
|
||||
{[
|
||||
{ label: 'UPPERCASE', value: upper },
|
||||
{ label: 'lowercase', value: lower },
|
||||
{ label: 'Title Case', value: title },
|
||||
{ label: 'Sentence case', value: sentence },
|
||||
{ label: "UPPERCASE", value: upper },
|
||||
{ label: "lowercase", value: lower },
|
||||
{ label: "Title Case", value: title },
|
||||
{ label: "Sentence case", value: sentence },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<code className="flex-1 truncate rounded border bg-muted/30 px-2 py-1 text-xs">{value || '—'}</code>
|
||||
<code className="flex-1 truncate rounded border bg-muted/30 px-2 py-1 text-xs">
|
||||
{value || "—"}
|
||||
</code>
|
||||
<span className="w-24 text-xs text-muted-foreground">{label}</span>
|
||||
<CopyButton text={value} />
|
||||
</div>
|
||||
@@ -52,73 +97,148 @@ function TextCaseConverter() {
|
||||
}
|
||||
|
||||
function CharacterCounter() {
|
||||
const [input, setInput] = useState('');
|
||||
const [input, setInput] = useState("");
|
||||
const chars = input.length;
|
||||
const charsNoSpaces = input.replace(/\s/g, '').length;
|
||||
const charsNoSpaces = input.replace(/\s/g, "").length;
|
||||
const words = input.trim() ? input.trim().split(/\s+/).length : 0;
|
||||
const lines = input ? input.split('\n').length : 0;
|
||||
const lines = input ? input.split("\n").length : 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div><Label>Text</Label><Textarea value={input} onChange={(e) => setInput(e.target.value)} rows={5} className="mt-1" placeholder="Introdu text..." /></div>
|
||||
<div>
|
||||
<Label>Text</Label>
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
rows={5}
|
||||
className="mt-1"
|
||||
placeholder="Introdu text..."
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Caractere</p><p className="text-xl font-bold">{chars}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Fără spații</p><p className="text-xl font-bold">{charsNoSpaces}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Cuvinte</p><p className="text-xl font-bold">{words}</p></CardContent></Card>
|
||||
<Card><CardContent className="p-3"><p className="text-xs text-muted-foreground">Linii</p><p className="text-xl font-bold">{lines}</p></CardContent></Card>
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<p className="text-xs text-muted-foreground">Caractere</p>
|
||||
<p className="text-xl font-bold">{chars}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<p className="text-xs text-muted-foreground">Fără spații</p>
|
||||
<p className="text-xl font-bold">{charsNoSpaces}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<p className="text-xs text-muted-foreground">Cuvinte</p>
|
||||
<p className="text-xl font-bold">{words}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-3">
|
||||
<p className="text-xs text-muted-foreground">Linii</p>
|
||||
<p className="text-xl font-bold">{lines}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PercentageCalculator() {
|
||||
const [value, setValue] = useState('');
|
||||
const [total, setTotal] = useState('');
|
||||
const [percent, setPercent] = useState('');
|
||||
const [value, setValue] = useState("");
|
||||
const [total, setTotal] = useState("");
|
||||
const [percent, setPercent] = useState("");
|
||||
|
||||
const v = parseFloat(value);
|
||||
const t = parseFloat(total);
|
||||
const p = parseFloat(percent);
|
||||
|
||||
const pctOfTotal = !isNaN(v) && !isNaN(t) && t !== 0 ? ((v / t) * 100).toFixed(2) : '—';
|
||||
const valFromPct = !isNaN(p) && !isNaN(t) ? ((p / 100) * t).toFixed(2) : '—';
|
||||
const pctOfTotal =
|
||||
!isNaN(v) && !isNaN(t) && t !== 0 ? ((v / t) * 100).toFixed(2) : "—";
|
||||
const valFromPct = !isNaN(p) && !isNaN(t) ? ((p / 100) * t).toFixed(2) : "—";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<div><Label>Valoare</Label><Input type="number" value={value} onChange={(e) => setValue(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>Total</Label><Input type="number" value={total} onChange={(e) => setTotal(e.target.value)} className="mt-1" /></div>
|
||||
<div><Label>Procent</Label><Input type="number" value={percent} onChange={(e) => setPercent(e.target.value)} className="mt-1" /></div>
|
||||
<div>
|
||||
<Label>Valoare</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Total</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={total}
|
||||
onChange={(e) => setTotal(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Procent</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={percent}
|
||||
onChange={(e) => setPercent(e.target.value)}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 rounded-md border bg-muted/30 p-3 text-sm">
|
||||
<p><strong>{value || '?'}</strong> din <strong>{total || '?'}</strong> = <strong>{pctOfTotal}%</strong></p>
|
||||
<p><strong>{percent || '?'}%</strong> din <strong>{total || '?'}</strong> = <strong>{valFromPct}</strong></p>
|
||||
<p>
|
||||
<strong>{value || "?"}</strong> din <strong>{total || "?"}</strong> ={" "}
|
||||
<strong>{pctOfTotal}%</strong>
|
||||
</p>
|
||||
<p>
|
||||
<strong>{percent || "?"}%</strong> din <strong>{total || "?"}</strong>{" "}
|
||||
= <strong>{valFromPct}</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AreaConverter() {
|
||||
const [mp, setMp] = useState('');
|
||||
const [mp, setMp] = useState("");
|
||||
const v = parseFloat(mp);
|
||||
|
||||
const conversions = !isNaN(v) ? [
|
||||
{ label: 'mp (m²)', value: v.toFixed(2) },
|
||||
{ label: 'ari (100 m²)', value: (v / 100).toFixed(4) },
|
||||
{ label: 'hectare (10.000 m²)', value: (v / 10000).toFixed(6) },
|
||||
{ label: 'km²', value: (v / 1000000).toFixed(8) },
|
||||
{ label: 'sq ft', value: (v * 10.7639).toFixed(2) },
|
||||
] : [];
|
||||
const conversions = !isNaN(v)
|
||||
? [
|
||||
{ label: "mp (m²)", value: v.toFixed(2) },
|
||||
{ label: "ari (100 m²)", value: (v / 100).toFixed(4) },
|
||||
{ label: "hectare (10.000 m²)", value: (v / 10000).toFixed(6) },
|
||||
{ label: "km²", value: (v / 1000000).toFixed(8) },
|
||||
{ label: "sq ft", value: (v * 10.7639).toFixed(2) },
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div><Label>Suprafață (m²)</Label><Input type="number" value={mp} onChange={(e) => setMp(e.target.value)} className="mt-1" placeholder="Introdu suprafața..." /></div>
|
||||
<div>
|
||||
<Label>Suprafață (m²)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={mp}
|
||||
onChange={(e) => setMp(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Introdu suprafața..."
|
||||
/>
|
||||
</div>
|
||||
{conversions.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
{conversions.map(({ label, value: val }) => (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<code className="flex-1 rounded border bg-muted/30 px-2 py-1 text-xs">{val}</code>
|
||||
<span className="w-36 text-xs text-muted-foreground">{label}</span>
|
||||
<code className="flex-1 rounded border bg-muted/30 px-2 py-1 text-xs">
|
||||
{val}
|
||||
</code>
|
||||
<span className="w-36 text-xs text-muted-foreground">
|
||||
{label}
|
||||
</span>
|
||||
<CopyButton text={val} />
|
||||
</div>
|
||||
))}
|
||||
@@ -128,31 +248,491 @@ function AreaConverter() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── U-value → R-value Converter ─────────────────────────────────────────────
|
||||
|
||||
function UValueConverter() {
|
||||
const [uValue, setUValue] = useState("");
|
||||
const [thickness, setThickness] = useState("");
|
||||
|
||||
const u = parseFloat(uValue);
|
||||
const t = parseFloat(thickness);
|
||||
const rValue = !isNaN(u) && u > 0 ? (1 / u).toFixed(4) : null;
|
||||
const rsi = 0.13;
|
||||
const rse = 0.04;
|
||||
const rTotal =
|
||||
rValue !== null ? (parseFloat(rValue) + rsi + rse).toFixed(4) : null;
|
||||
const lambda =
|
||||
rValue !== null && !isNaN(t) && t > 0
|
||||
? (t / 100 / parseFloat(rValue)).toFixed(4)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>Coeficient U (W/m²K)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
value={uValue}
|
||||
onChange={(e) => setUValue(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="ex: 0.35"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Grosime material (cm) — opțional</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
value={thickness}
|
||||
onChange={(e) => setThickness(e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="ex: 20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{rValue !== null && (
|
||||
<div className="space-y-2 rounded-md border bg-muted/30 p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">R = 1/U</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="rounded border bg-muted px-2 py-0.5">
|
||||
{rValue} m²K/W
|
||||
</code>
|
||||
<CopyButton text={rValue} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span>Rsi (suprafață interioară)</span>
|
||||
<code className="rounded border bg-muted px-2 py-0.5">
|
||||
{rsi} m²K/W
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span>Rse (suprafață exterioară)</span>
|
||||
<code className="rounded border bg-muted px-2 py-0.5">
|
||||
{rse} m²K/W
|
||||
</code>
|
||||
</div>
|
||||
<div className="flex items-center justify-between font-medium border-t pt-2 mt-1">
|
||||
<span>R total (cu Rsi + Rse)</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="rounded border bg-muted px-2 py-0.5">
|
||||
{rTotal} m²K/W
|
||||
</code>
|
||||
<CopyButton text={rTotal ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
{lambda !== null && (
|
||||
<div className="flex items-center justify-between text-muted-foreground border-t pt-2 mt-1">
|
||||
<span>Conductivitate λ = d/R</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<code className="rounded border bg-muted px-2 py-0.5">
|
||||
{lambda} W/mK
|
||||
</code>
|
||||
<CopyButton text={lambda} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── AI Artifact Cleaner ──────────────────────────────────────────────────────
|
||||
|
||||
function AiArtifactCleaner() {
|
||||
const [input, setInput] = useState("");
|
||||
|
||||
const clean = (text: string): string => {
|
||||
let r = text;
|
||||
// Strip markdown
|
||||
r = r.replace(/^#{1,6}\s+/gm, "");
|
||||
r = r.replace(/\*\*(.+?)\*\*/g, "$1");
|
||||
r = r.replace(/\*(.+?)\*/g, "$1");
|
||||
r = r.replace(/_{2}(.+?)_{2}/g, "$1");
|
||||
r = r.replace(/_(.+?)_/g, "$1");
|
||||
r = r.replace(/```[\s\S]*?```/g, "");
|
||||
r = r.replace(/`(.+?)`/g, "$1");
|
||||
r = r.replace(/^[*\-+]\s+/gm, "");
|
||||
r = r.replace(/^\d+\.\s+/gm, "");
|
||||
r = r.replace(/^[-_*]{3,}$/gm, "");
|
||||
r = r.replace(/\[(.+?)\]\(.*?\)/g, "$1");
|
||||
r = r.replace(/^>\s+/gm, "");
|
||||
// Fix encoding artifacts (UTF-8 mojibake)
|
||||
r = r.replace(/â/g, "â");
|
||||
r = r.replace(/î/g, "î");
|
||||
r = r.replace(/Ã /g, "à");
|
||||
r = r.replace(/Å£/g, "ț");
|
||||
r = r.replace(/È™/g, "ș");
|
||||
r = r.replace(/È›/g, "ț");
|
||||
r = r.replace(/Èš/g, "Ț");
|
||||
r = r.replace(/\u015f/g, "ș");
|
||||
r = r.replace(/\u0163/g, "ț");
|
||||
// Remove zero-width and invisible chars
|
||||
r = r.replace(/[\u200b\u200c\u200d\ufeff]/g, "");
|
||||
// Normalize typography
|
||||
r = r.replace(/[""]/g, '"');
|
||||
r = r.replace(/['']/g, "'");
|
||||
r = r.replace(/[–—]/g, "-");
|
||||
r = r.replace(/…/g, "...");
|
||||
// Normalize spacing
|
||||
r = r.replace(/ {2,}/g, " ");
|
||||
r = r.replace(/\n{3,}/g, "\n\n");
|
||||
return r.trim();
|
||||
};
|
||||
|
||||
const cleaned = input ? clean(input) : "";
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>Text original (output AI)</Label>
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
className="mt-1 h-72 font-mono text-xs"
|
||||
placeholder="Lipește textul generat de AI..."
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Text curățat</Label>
|
||||
{cleaned && <CopyButton text={cleaned} />}
|
||||
</div>
|
||||
<Textarea
|
||||
value={cleaned}
|
||||
readOnly
|
||||
className="mt-1 h-72 font-mono text-xs bg-muted/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Operații: eliminare markdown (###, **, `, liste, citate), corectare
|
||||
encoding românesc (mojibake), curățare Unicode invizibil, normalizare
|
||||
ghilimele / cratime / spații multiple.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── MDLPA Date Locale ────────────────────────────────────────────────────────
|
||||
|
||||
function MdlpaValidator() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
<a
|
||||
href="https://datelocale.mdlpa.ro"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline underline-offset-2"
|
||||
>
|
||||
Deschide datelocale.mdlpa.ro ↗
|
||||
</a>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<a
|
||||
href="https://datelocale.mdlpa.ro/ro/about/tutorials"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline underline-offset-2"
|
||||
>
|
||||
Tutoriale video ↗
|
||||
</a>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<a
|
||||
href="https://datelocale.mdlpa.ro/ro/about/info/reguli"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline underline-offset-2"
|
||||
>
|
||||
Reguli de calcul ↗
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-hidden rounded-md border"
|
||||
style={{ height: "560px" }}
|
||||
>
|
||||
<iframe
|
||||
src="https://datelocale.mdlpa.ro"
|
||||
className="h-full w-full"
|
||||
title="MDLPA — Date Locale"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── PDF Reducer (Stirling PDF) ───────────────────────────────────────────────
|
||||
|
||||
function PdfReducer() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [optimizeLevel, setOptimizeLevel] = useState("2");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleCompress = async () => {
|
||||
if (!file) return;
|
||||
setLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("optimizeLevel", optimizeLevel);
|
||||
const res = await fetch(
|
||||
"http://10.10.10.166:8087/api/v1/misc/compress-pdf",
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
},
|
||||
);
|
||||
if (!res.ok) throw new Error(`Eroare server: ${res.status}`);
|
||||
const blob = await res.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = file.name.replace(/\.pdf$/i, "-comprimat.pdf");
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
setError(
|
||||
"Nu s-a putut conecta la Stirling PDF. Folosește linkul de mai jos pentru a deschide aplicația manual.",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label>Fișier PDF</Label>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
setFile(e.target.files?.[0] ?? null);
|
||||
setError("");
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="outline" onClick={() => fileRef.current?.click()}>
|
||||
Selectează PDF...
|
||||
</Button>
|
||||
{file && (
|
||||
<span className="text-sm text-muted-foreground">{file.name}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label>Nivel compresie</Label>
|
||||
<select
|
||||
value={optimizeLevel}
|
||||
onChange={(e) => setOptimizeLevel(e.target.value)}
|
||||
className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="0">0 — fără modificări (test)</option>
|
||||
<option value="1">1 — compresie minimă</option>
|
||||
<option value="2">2 — echilibrat (recomandat)</option>
|
||||
<option value="3">3 — compresie mare</option>
|
||||
<option value="4">4 — compresie maximă</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={handleCompress} disabled={!file || loading}>
|
||||
{loading ? "Se comprimă..." : "Comprimă PDF"}
|
||||
</Button>
|
||||
<Button variant="ghost" asChild>
|
||||
<a
|
||||
href="http://10.10.10.166:8087/compress-pdf"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Deschide Stirling PDF ↗
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Quick OCR ────────────────────────────────────────────────────────────────
|
||||
|
||||
function QuickOcr() {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-3 text-sm">
|
||||
<a
|
||||
href="https://ocr.z.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary underline underline-offset-2"
|
||||
>
|
||||
Deschide ocr.z.ai ↗
|
||||
</a>
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
Extragere text din imagini și PDF-uri scanate
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-hidden rounded-md border"
|
||||
style={{ height: "560px" }}
|
||||
>
|
||||
<iframe
|
||||
src="https://ocr.z.ai"
|
||||
className="h-full w-full"
|
||||
title="OCR — extragere text din imagini"
|
||||
allow="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Module ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function MiniUtilitiesModule() {
|
||||
return (
|
||||
<Tabs defaultValue="text-case" className="space-y-4">
|
||||
<TabsList className="flex-wrap">
|
||||
<TabsTrigger value="text-case"><Type className="mr-1 h-3.5 w-3.5" /> Transformare text</TabsTrigger>
|
||||
<TabsTrigger value="char-count"><Hash className="mr-1 h-3.5 w-3.5" /> Numărare caractere</TabsTrigger>
|
||||
<TabsTrigger value="percentage"><Percent className="mr-1 h-3.5 w-3.5" /> Procente</TabsTrigger>
|
||||
<TabsTrigger value="area"><Ruler className="mr-1 h-3.5 w-3.5" /> Convertor suprafețe</TabsTrigger>
|
||||
<TabsTrigger value="text-case">
|
||||
<Type className="mr-1 h-3.5 w-3.5" /> Transformare text
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="char-count">
|
||||
<Hash className="mr-1 h-3.5 w-3.5" /> Numărare caractere
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="percentage">
|
||||
<Percent className="mr-1 h-3.5 w-3.5" /> Procente
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="area">
|
||||
<Ruler className="mr-1 h-3.5 w-3.5" /> Suprafețe
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="u-value">
|
||||
<Zap className="mr-1 h-3.5 w-3.5" /> U → R
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ai-cleaner">
|
||||
<Wand2 className="mr-1 h-3.5 w-3.5" /> Curățare AI
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="mdlpa">
|
||||
<Building2 className="mr-1 h-3.5 w-3.5" /> MDLPA
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pdf-reducer">
|
||||
<FileDown className="mr-1 h-3.5 w-3.5" /> Reducere PDF
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ocr">
|
||||
<ScanText className="mr-1 h-3.5 w-3.5" /> OCR
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="text-case">
|
||||
<Card><CardHeader><CardTitle className="text-base">Transformare text</CardTitle></CardHeader>
|
||||
<CardContent><TextCaseConverter /></CardContent></Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Transformare text</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TextCaseConverter />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="char-count">
|
||||
<Card><CardHeader><CardTitle className="text-base">Numărare caractere</CardTitle></CardHeader>
|
||||
<CardContent><CharacterCounter /></CardContent></Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Numărare caractere</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CharacterCounter />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="percentage">
|
||||
<Card><CardHeader><CardTitle className="text-base">Calculator procente</CardTitle></CardHeader>
|
||||
<CardContent><PercentageCalculator /></CardContent></Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Calculator procente</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PercentageCalculator />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="area">
|
||||
<Card><CardHeader><CardTitle className="text-base">Convertor suprafețe</CardTitle></CardHeader>
|
||||
<CardContent><AreaConverter /></CardContent></Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Convertor suprafețe</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AreaConverter />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="u-value">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
Convertor U → R (termoizolație)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UValueConverter />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="ai-cleaner">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Curățare text AI</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AiArtifactCleaner />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="mdlpa">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
MDLPA — Date locale construcții
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<MdlpaValidator />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="pdf-reducer">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Reducere dimensiune PDF</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<PdfReducer />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="ocr">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
OCR — extragere text din imagini
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<QuickOcr />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user