"use client";
import { useState, useRef, useCallback, useEffect } from "react";
import {
Copy,
Check,
Hash,
Type,
Percent,
Ruler,
Zap,
Wand2,
Building2,
FileDown,
ScanText,
CaseUpper,
Palette,
Upload,
Receipt,
Layers,
ArrowUp,
ArrowDown,
Unlock,
PenTool,
Maximize2,
} 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);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
/* silent */
}
};
return (
{copied ? (
) : (
)}
);
}
function TextCaseConverter() {
const [input, setInput] = useState("");
const upper = input.toUpperCase();
const lower = input.toLowerCase();
const title = input.replace(/\b\w/g, (c) => c.toUpperCase());
const sentence = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase();
return (
Text sursă
{[
{ label: "UPPERCASE", value: upper },
{ label: "lowercase", value: lower },
{ label: "Title Case", value: title },
{ label: "Sentence case", value: sentence },
].map(({ label, value }) => (
{value || "—"}
{label}
))}
);
}
function CharacterCounter() {
const [input, setInput] = useState("");
const chars = input.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;
return (
Text
Caractere
{chars}
Fără spații
{charsNoSpaces}
Cuvinte
{words}
Linii
{lines}
);
}
function PercentageCalculator() {
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) : "—";
return (
{value || "?"} din {total || "?"} ={" "}
{pctOfTotal}%
{percent || "?"}% din {total || "?"} {" "}
= {valFromPct}
);
}
const TVA_PRESETS = [5, 9, 19, 21];
function TvaCalculator() {
const [tvaRate, setTvaRate] = useState(19);
const [customRate, setCustomRate] = useState("");
const [amount, setAmount] = useState("");
const [mode, setMode] = useState<"add" | "extract">("add");
const effectiveRate =
customRate !== "" ? parseFloat(customRate) || tvaRate : tvaRate;
const val = parseFloat(amount);
const tvaMultiplier = effectiveRate / 100;
const cuTva = !isNaN(val) && mode === "add" ? val * (1 + tvaMultiplier) : NaN;
const faraTva =
!isNaN(val) && mode === "extract" ? val / (1 + tvaMultiplier) : NaN;
const tvaAmount = !isNaN(val)
? mode === "add"
? val * tvaMultiplier
: val - val / (1 + tvaMultiplier)
: NaN;
const fmt = (n: number) =>
isNaN(n)
? "—"
: n.toLocaleString("ro-RO", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return (
{/* Rate selector */}
Cotă TVA
{TVA_PRESETS.map((r) => (
{ setTvaRate(r); setCustomRate(""); }}
>
{r}%
))}
setCustomRate(e.target.value)}
placeholder="Altă cotă"
className="w-28 text-sm"
/>
{customRate && (
%
)}
setMode("add")}
>
Adaugă TVA
setMode("extract")}
>
Extrage TVA
{!isNaN(val) && val > 0 && (
{mode === "add" ? (
<>
Sumă fără TVA: {fmt(val)} RON
TVA ({effectiveRate}%): {fmt(tvaAmount)} RON
Total cu TVA:{" "}
{fmt(cuTva)} RON
>
) : (
<>
Sumă cu TVA: {fmt(val)} RON
TVA ({effectiveRate}%): {fmt(tvaAmount)} RON
Sumă fără TVA:{" "}
{fmt(faraTva)} RON
>
)}
)}
);
}
function AreaConverter() {
const units = [
{ key: "mp", label: "mp (m²)", factor: 1 },
{ key: "ari", label: "ari (100 m²)", factor: 100 },
{ key: "ha", label: "hectare (10.000 m²)", factor: 10000 },
{ key: "km2", label: "km²", factor: 1000000 },
{ key: "sqft", label: "sq ft", factor: 1 / 10.7639 },
] as const;
const [values, setValues] = useState>({});
const [activeField, setActiveField] = useState(null);
const handleChange = (key: string, raw: string) => {
if (raw === "") {
setValues({});
return;
}
const v = parseFloat(raw);
if (isNaN(v)) return;
const unit = units.find((u) => u.key === key);
if (!unit) return;
const mp = v * unit.factor;
const next: Record = {};
for (const u of units) {
next[u.key] =
u.key === key
? raw
: (mp / u.factor).toPrecision(8).replace(/\.?0+$/, "");
}
setValues(next);
};
return (
Introdu o valoare în orice câmp — celelalte se calculează automat.
{units.map((u) => (
{u.label}
setActiveField(u.key)}
onBlur={() => setActiveField(null)}
onChange={(e) => handleChange(u.key, e.target.value)}
className={`flex-1 ${activeField === u.key ? "ring-1 ring-primary" : ""}`}
placeholder="0"
/>
))}
);
}
// ─── U-value → R-value Converter ─────────────────────────────────────────────
function UValueConverter() {
const [mode, setMode] = useState<"u-to-r" | "r-to-u">("u-to-r");
const [uValue, setUValue] = useState("");
const [rInput, setRInput] = useState("");
const [thickness, setThickness] = useState("");
const rsi = 0.13;
const rse = 0.04;
// U → R
const u = parseFloat(uValue);
const rFromU = !isNaN(u) && u > 0 ? (1 / u).toFixed(4) : null;
const rTotalFromU =
rFromU !== null ? (parseFloat(rFromU) + rsi + rse).toFixed(4) : null;
// R → U
const rIn = parseFloat(rInput);
const uFromR = !isNaN(rIn) && rIn > 0 ? (1 / rIn).toFixed(4) : null;
const rTotalFromR = uFromR !== null ? (rIn + rsi + rse).toFixed(4) : null;
const activeR = mode === "u-to-r" ? rFromU : rInput || null;
const activeU = mode === "u-to-r" ? uValue || null : uFromR;
const activeRTotal = mode === "u-to-r" ? rTotalFromU : rTotalFromR;
const t = parseFloat(thickness);
const lambda =
activeR !== null && !isNaN(t) && t > 0
? (t / 100 / parseFloat(activeR)).toFixed(4)
: null;
return (
{
setMode("u-to-r");
setRInput("");
}}
>
U → R
{
setMode("r-to-u");
setUValue("");
}}
>
R → U
{(activeR !== null || activeU !== null) && (
{mode === "u-to-r" && rFromU !== null && (
)}
{mode === "r-to-u" && uFromR !== null && (
)}
Rsi (suprafață interioară)
{rsi} m²K/W
Rse (suprafață exterioară)
{rse} m²K/W
{activeRTotal !== null && (
R total (cu Rsi + Rse)
{activeRTotal} m²K/W
)}
{lambda !== null && (
Conductivitate λ = d/R
{lambda} W/mK
)}
)}
);
}
// ─── Material Thermal Comparison ─────────────────────────────────────────────
interface ThermalMaterial {
id: string;
name: string;
lambda: number; // W/mK — design value (λD) from Romanian technical sheets
category: string;
source: string; // brand / standard reference
}
// λD values from Romanian-market technical data sheets (fișe tehnice)
// Sources: Austrotherm, ISOVER, Knauf Insulation, Ytong/Xella, Porotherm/Wienerberger,
// Baumit, Caparol, Tondach, SR EN ISO 10456, C107
const THERMAL_MATERIALS: ThermalMaterial[] = [
// Termoizolații
{
id: "eps-70",
name: "Polistiren expandat EPS 70",
lambda: 0.037,
category: "Termoizolații",
source: "Austrotherm EPS A70",
},
{
id: "eps-80",
name: "Polistiren expandat EPS 80",
lambda: 0.035,
category: "Termoizolații",
source: "Austrotherm EPS A80",
},
{
id: "eps-100",
name: "Polistiren expandat EPS 100",
lambda: 0.033,
category: "Termoizolații",
source: "Austrotherm EPS A100",
},
{
id: "eps-graf",
name: "Polistiren grafitat (EPS-G)",
lambda: 0.031,
category: "Termoizolații",
source: "Austrotherm EPS A150 Grafit",
},
{
id: "xps-30",
name: "Polistiren extrudat XPS 30",
lambda: 0.034,
category: "Termoizolații",
source: "Austrotherm XPS TOP 30",
},
{
id: "xps-50",
name: "Polistiren extrudat XPS 50",
lambda: 0.036,
category: "Termoizolații",
source: "Austrotherm XPS TOP 50",
},
{
id: "vata-min",
name: "Vată minerală bazaltică",
lambda: 0.036,
category: "Termoizolații",
source: "ISOVER PLE / Knauf TP 136",
},
{
id: "vata-stc",
name: "Vată minerală de sticlă",
lambda: 0.035,
category: "Termoizolații",
source: "ISOVER Uniroll Plus",
},
{
id: "pu-spray",
name: "Spumă poliuretanică (PUR)",
lambda: 0.024,
category: "Termoizolații",
source: "Basf Elastopor",
},
{
id: "pir",
name: "Plăci PIR (poliizocianurat)",
lambda: 0.022,
category: "Termoizolații",
source: "Pir/Kingspan",
},
// Zidărie
{
id: "bca-35",
name: "BCA densitate 350 kg/m³",
lambda: 0.09,
category: "Zidărie",
source: "Ytong A+ PP2/0.35",
},
{
id: "bca-40",
name: "BCA densitate 400 kg/m³",
lambda: 0.1,
category: "Zidărie",
source: "Ytong / Celco 400",
},
{
id: "bca-50",
name: "BCA densitate 500 kg/m³",
lambda: 0.13,
category: "Zidărie",
source: "Celco A+ 500",
},
{
id: "bca-60",
name: "BCA densitate 600 kg/m³",
lambda: 0.16,
category: "Zidărie",
source: "Ytong PP4/0.6",
},
{
id: "poro-30",
name: "Porotherm 30 N+F",
lambda: 0.21,
category: "Zidărie",
source: "Wienerberger Porotherm 30",
},
{
id: "poro-38",
name: "Porotherm 38 Thermo",
lambda: 0.16,
category: "Zidărie",
source: "Wienerberger Porotherm 38 Thermo",
},
{
id: "poro-44",
name: "Porotherm 44 Thermo",
lambda: 0.13,
category: "Zidărie",
source: "Wienerberger Porotherm 44 Thermo",
},
{
id: "car-plin",
name: "Cărămidă plină presată",
lambda: 0.74,
category: "Zidărie",
source: "C107 / SR EN ISO 10456",
},
{
id: "car-gol",
name: "Cărămidă cu goluri G25",
lambda: 0.46,
category: "Zidărie",
source: "C107 / SR EN ISO 10456",
},
// Tencuieli / Finisaje
{
id: "tenc-term",
name: "Tencuială termoizolantă",
lambda: 0.08,
category: "Tencuieli",
source: "Baumit ThermoExtra",
},
{
id: "tenc-var",
name: "Tencuială var-ciment",
lambda: 0.87,
category: "Tencuieli",
source: "C107 / SR EN ISO 10456",
},
{
id: "tenc-cem",
name: "Mortar de ciment",
lambda: 1.0,
category: "Tencuieli",
source: "C107 / SR EN ISO 10456",
},
{
id: "gips-cart",
name: "Gips-carton",
lambda: 0.25,
category: "Tencuieli",
source: "Knauf / Rigips",
},
{
id: "tenc-dec",
name: "Tencuială decorativă",
lambda: 0.7,
category: "Tencuieli",
source: "Baumit / Caparol",
},
// Structură
{
id: "beton",
name: "Beton armat",
lambda: 1.74,
category: "Structură",
source: "C107 / SR EN ISO 10456",
},
{
id: "lemn-mol",
name: "Lemn molid/brad",
lambda: 0.14,
category: "Structură",
source: "C107 / SR EN ISO 10456",
},
{
id: "osb",
name: "Plăci OSB",
lambda: 0.13,
category: "Structură",
source: "Egger / Kronospan",
},
{
id: "clt",
name: "Lemn CLT (cross-laminated)",
lambda: 0.12,
category: "Structură",
source: "Stora Enso / Binderholz",
},
// Hidroizolații / Bariere
{
id: "bpv",
name: "Barieră de vapori (PE)",
lambda: 0.4,
category: "Bariere/Hidro",
source: "Bramac / Dörken Delta",
},
{
id: "hidro-bit",
name: "Membrană bituminoasă",
lambda: 0.23,
category: "Bariere/Hidro",
source: "IKO Armourbase / Siplast",
},
{
id: "hidro-pvc",
name: "Membrană PVC/TPO",
lambda: 0.16,
category: "Bariere/Hidro",
source: "Sika Sarnafil / Renolit",
},
{
id: "folie-difuz",
name: "Folie difuzie (sub-acoperiș)",
lambda: 0.4,
category: "Bariere/Hidro",
source: "Bramac Pro / Dörken",
},
// Pardoseli / Învelitori
{
id: "sapa-cem",
name: "Șapă ciment",
lambda: 1.4,
category: "Pardoseli",
source: "C107 / SR EN ISO 10456",
},
{
id: "sapa-anh",
name: "Șapă anhidrit",
lambda: 1.2,
category: "Pardoseli",
source: "Knauf FE 80 Eco",
},
{
id: "gresie",
name: "Gresie/Faianță ceramică",
lambda: 1.3,
category: "Pardoseli",
source: "C107 / SR EN ISO 10456",
},
{
id: "parchet",
name: "Parchet lemn stejar",
lambda: 0.18,
category: "Pardoseli",
source: "C107 / SR EN ISO 10456",
},
{
id: "tigla-cer",
name: "Țiglă ceramică",
lambda: 1.0,
category: "Pardoseli",
source: "Tondach / Bramac",
},
{
id: "tigla-bet",
name: "Țiglă beton",
lambda: 1.5,
category: "Pardoseli",
source: "Bramac",
},
{
id: "tabl-falt",
name: "Tablă fălțuită (oțel)",
lambda: 50.0,
category: "Pardoseli",
source: "Lindab / Bilka",
},
// Umpluturi
{
id: "pietris",
name: "Pietriș/Balast compactat",
lambda: 2.0,
category: "Umpluturi",
source: "C107 / SR EN ISO 10456",
},
{
id: "nisip",
name: "Nisip compactat",
lambda: 2.0,
category: "Umpluturi",
source: "C107 / SR EN ISO 10456",
},
{
id: "argila-exp",
name: "Argilă expandată (Leca)",
lambda: 0.1,
category: "Umpluturi",
source: "Leca / Weber Saint-Gobain",
},
{
id: "aer-inch",
name: "Strat de aer închis (ventilat)",
lambda: 0.025,
category: "Umpluturi",
source: "C107 (Rconvențional)",
},
];
interface LayerEntry {
materialId: string;
thickness: number; // cm
}
interface CompositionTemplate {
name: string;
description: string;
category: string;
layers: LayerEntry[];
}
const COMPOSITION_TEMPLATES: CompositionTemplate[] = [
// Pereți exteriori
{
name: "Perete BCA 30 + EPS 10",
description: "Sistem clasic ETICS cu BCA",
category: "Pereți exteriori",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "bca-40", thickness: 30 },
{ materialId: "eps-80", thickness: 10 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
{
name: "Perete BCA 30 + vată 10",
description: "ETICS cu vată minerală (A1 foc)",
category: "Pereți exteriori",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "bca-40", thickness: 30 },
{ materialId: "vata-min", thickness: 10 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
{
name: "Perete Porotherm 30 + EPS 10",
description: "Ceramică + polistiren",
category: "Pereți exteriori",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "poro-30", thickness: 30 },
{ materialId: "eps-80", thickness: 10 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
{
name: "Perete Porotherm 38 Thermo neizolat",
description: "Bloc ceramic termoizolant, fără ETICS",
category: "Pereți exteriori",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "poro-38", thickness: 38 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
{
name: "Perete Porotherm 44 Thermo neizolat",
description: "Bloc ceramic termoizolant gros",
category: "Pereți exteriori",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "poro-44", thickness: 44 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
{
name: "Perete cărămidă + tencuială termoizolantă",
description: "Renovare cu termoizolantă Baumit",
category: "Pereți exteriori",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "car-gol", thickness: 25 },
{ materialId: "tenc-term", thickness: 8 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
{
name: "Perete lemn CLT + vată",
description: "Structură CLT cu izolație exterioară",
category: "Pereți exteriori",
layers: [
{ materialId: "gips-cart", thickness: 1.25 },
{ materialId: "clt", thickness: 10 },
{ materialId: "vata-min", thickness: 15 },
{ materialId: "folie-difuz", thickness: 0.05 },
{ materialId: "tenc-dec", thickness: 0.5 },
],
},
// Pereți subsol
{
name: "Perete subsol beton + XPS",
description: "Fundație beton cu izolație XPS în pământ",
category: "Pereți subsol",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "beton", thickness: 25 },
{ materialId: "hidro-bit", thickness: 0.5 },
{ materialId: "xps-30", thickness: 10 },
],
},
{
name: "Perete subsol beton + XPS 15",
description: "Fundație cu izolație sporită",
category: "Pereți subsol",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "beton", thickness: 30 },
{ materialId: "hidro-bit", thickness: 0.5 },
{ materialId: "xps-30", thickness: 15 },
],
},
// Terase / Acoperișuri plate
{
name: "Terasă necirculabilă — clasic",
description: "Placa beton + termoizolație + hidroizolație",
category: "Terase",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "eps-100", thickness: 15 },
{ materialId: "sapa-cem", thickness: 4 },
{ materialId: "hidro-bit", thickness: 1 },
],
},
{
name: "Terasă necirculabilă — PIR",
description: "Cu PIR pentru grosime minimă",
category: "Terase",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "pir", thickness: 10 },
{ materialId: "sapa-cem", thickness: 4 },
{ materialId: "hidro-bit", thickness: 1 },
],
},
{
name: "Terasă circulabilă — gresie",
description: "Terasă cu finisaj gresie pe plot",
category: "Terase",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "eps-100", thickness: 15 },
{ materialId: "sapa-cem", thickness: 5 },
{ materialId: "hidro-bit", thickness: 1 },
{ materialId: "gresie", thickness: 2 },
],
},
{
name: "Acoperiș verde extensiv",
description: "Strat vegetal pe terasă",
category: "Terase",
layers: [
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "xps-30", thickness: 15 },
{ materialId: "sapa-cem", thickness: 4 },
{ materialId: "hidro-pvc", thickness: 0.2 },
{ materialId: "argila-exp", thickness: 8 },
],
},
// Șarpante
{
name: "Șarpantă lemn + vată între căpriori",
description: "Izolație între și sub căpriori, clasic",
category: "Șarpante",
layers: [
{ materialId: "gips-cart", thickness: 1.25 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "vata-min", thickness: 20 },
{ materialId: "osb", thickness: 1.2 },
{ materialId: "folie-difuz", thickness: 0.05 },
{ materialId: "aer-inch", thickness: 4 },
{ materialId: "tigla-cer", thickness: 2 },
],
},
{
name: "Șarpantă lemn + vată 30cm",
description: "Izolație sporită suprapusă",
category: "Șarpante",
layers: [
{ materialId: "gips-cart", thickness: 1.25 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "vata-min", thickness: 30 },
{ materialId: "osb", thickness: 1.2 },
{ materialId: "folie-difuz", thickness: 0.05 },
{ materialId: "aer-inch", thickness: 4 },
{ materialId: "tigla-cer", thickness: 2 },
],
},
{
name: "Șarpantă cu tablă fălțuită",
description: "Acoperiș metalic cu izolație vată",
category: "Șarpante",
layers: [
{ materialId: "gips-cart", thickness: 1.25 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "vata-min", thickness: 25 },
{ materialId: "osb", thickness: 1.2 },
{ materialId: "folie-difuz", thickness: 0.05 },
{ materialId: "aer-inch", thickness: 4 },
{ materialId: "tabl-falt", thickness: 0.06 },
],
},
// Plăci pe sol
{
name: "Placă pe sol — EPS sub placă",
description: "Izolație sub radier, clasic",
category: "Plăci pe sol",
layers: [
{ materialId: "parchet", thickness: 1.5 },
{ materialId: "sapa-cem", thickness: 6 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "eps-100", thickness: 10 },
{ materialId: "hidro-bit", thickness: 0.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "pietris", thickness: 15 },
],
},
{
name: "Placă pe sol — XPS sub placă",
description: "XPS rezistent la umiditate",
category: "Plăci pe sol",
layers: [
{ materialId: "gresie", thickness: 1 },
{ materialId: "sapa-cem", thickness: 6 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "xps-30", thickness: 10 },
{ materialId: "hidro-bit", thickness: 0.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "pietris", thickness: 15 },
],
},
{
name: "Placă pe sol — izolație sporită 15cm",
description: "Pentru clădiri nZEB",
category: "Plăci pe sol",
layers: [
{ materialId: "parchet", thickness: 1.5 },
{ materialId: "sapa-cem", thickness: 6 },
{ materialId: "bpv", thickness: 0.05 },
{ materialId: "xps-30", thickness: 15 },
{ materialId: "hidro-bit", thickness: 0.5 },
{ materialId: "beton", thickness: 15 },
{ materialId: "pietris", thickness: 15 },
],
},
];
function MaterialThermalComparison() {
const [layersA, setLayersA] = useState([
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "bca-40", thickness: 30 },
{ materialId: "vata-min", thickness: 10 },
{ materialId: "tenc-dec", thickness: 0.5 },
]);
const [layersB, setLayersB] = useState([
{ materialId: "tenc-var", thickness: 1.5 },
{ materialId: "poro-38", thickness: 38 },
{ materialId: "tenc-dec", thickness: 0.5 },
]);
// Drag-and-drop reorder state
const dragIdxRef = useRef(null);
const [dragOverIdx, setDragOverIdx] = useState(null);
const rsi = 0.13;
const rse = 0.04;
const categories = Array.from(
new Set(THERMAL_MATERIALS.map((m) => m.category)),
);
const templateCategories = Array.from(
new Set(COMPOSITION_TEMPLATES.map((t) => t.category)),
);
const calcR = (layers: LayerEntry[]): number => {
let r = 0;
for (const layer of layers) {
const mat = THERMAL_MATERIALS.find((m) => m.id === layer.materialId);
if (mat && layer.thickness > 0) {
r += layer.thickness / 100 / mat.lambda;
}
}
return r;
};
const rA = calcR(layersA);
const rB = calcR(layersB);
const rTotalA = rA + rsi + rse;
const rTotalB = rB + rsi + rse;
const uA = rTotalA > 0 ? 1 / rTotalA : 0;
const uB = rTotalB > 0 ? 1 / rTotalB : 0;
const updateLayer = (
setLayers: React.Dispatch>,
idx: number,
field: "materialId" | "thickness",
value: string,
) => {
setLayers((prev) => {
const next = [...prev];
const entry = next[idx];
if (!entry) return prev;
if (field === "materialId") {
next[idx] = { ...entry, materialId: value };
} else {
next[idx] = { ...entry, thickness: parseFloat(value) || 0 };
}
return next;
});
};
const addLayer = (
setLayers: React.Dispatch>,
) => {
setLayers((prev) => [...prev, { materialId: "eps-80", thickness: 10 }]);
};
const removeLayer = (
setLayers: React.Dispatch>,
idx: number,
) => {
setLayers((prev) => prev.filter((_, i) => i !== idx));
};
const moveLayer = (
setLayers: React.Dispatch>,
idx: number,
direction: "up" | "down",
) => {
setLayers((prev) => {
const next = [...prev];
const targetIdx = direction === "up" ? idx - 1 : idx + 1;
if (targetIdx < 0 || targetIdx >= next.length) return prev;
const a = next[idx];
const b = next[targetIdx];
if (!a || !b) return prev;
next[idx] = b;
next[targetIdx] = a;
return next;
});
};
const applyTemplate = (
setLayers: React.Dispatch>,
template: CompositionTemplate,
) => {
setLayers(template.layers.map((l) => ({ ...l })));
};
const renderComposition = (
label: string,
layers: LayerEntry[],
setLayers: React.Dispatch>,
r: number,
rTotal: number,
u: number,
) => (
{label}
{
const tpl = COMPOSITION_TEMPLATES.find(
(t) => t.name === e.target.value,
);
if (tpl) applyTemplate(setLayers, tpl);
e.target.value = "";
}}
value=""
className="rounded-md border border-border bg-background px-2 py-1 text-xs max-w-[200px]"
>
Șablon...
{templateCategories.map((cat) => (
{COMPOSITION_TEMPLATES.filter((t) => t.category === cat).map(
(t) => (
{t.name}
),
)}
))}
{/* Interior label */}
{layers.map((layer, idx) => {
const mat = THERMAL_MATERIALS.find((m) => m.id === layer.materialId);
return (
{
dragIdxRef.current = idx;
e.dataTransfer.effectAllowed = "move";
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
setDragOverIdx(idx);
}}
onDragLeave={() => setDragOverIdx(null)}
onDrop={(e) => {
e.preventDefault();
const from = dragIdxRef.current;
if (from !== null && from !== idx) {
setLayers((prev) => {
const next = [...prev];
const item = next[from];
if (!item) return prev;
next.splice(from, 1);
next.splice(idx, 0, item);
return next;
});
}
dragIdxRef.current = null;
setDragOverIdx(null);
}}
onDragEnd={() => {
dragIdxRef.current = null;
setDragOverIdx(null);
}}
className={`flex gap-1.5 items-end cursor-grab active:cursor-grabbing transition-all ${
dragOverIdx === idx
? "border-t-2 border-primary"
: "border-t-2 border-transparent"
}`}
>
{/* Move up/down */}
{idx === 0 && (
↕
)}
moveLayer(setLayers, idx, "up")}
disabled={idx === 0}
className="h-4 w-5 flex items-center justify-center text-muted-foreground hover:text-foreground disabled:opacity-20 disabled:cursor-default"
>
moveLayer(setLayers, idx, "down")}
disabled={idx === layers.length - 1}
className="h-4 w-5 flex items-center justify-center text-muted-foreground hover:text-foreground disabled:opacity-20 disabled:cursor-default"
>
{idx === 0 && (
Material
)}
updateLayer(setLayers, idx, "materialId", e.target.value)
}
className="mt-0.5 w-full rounded-md border border-border bg-background px-2 py-1.5 text-sm"
>
{categories.map((cat) => (
{THERMAL_MATERIALS.filter((m) => m.category === cat).map(
(m) => (
{m.name} (λ={m.lambda})
),
)}
))}
{idx === 0 && (
cm
)}
updateLayer(setLayers, idx, "thickness", e.target.value)
}
className="mt-0.5"
/>
{idx === 0 && (
R
)}
{mat && layer.thickness > 0
? (layer.thickness / 100 / mat.lambda).toFixed(3)
: "—"}
{layers.length > 1 && (
removeLayer(setLayers, idx)}
>
×
)}
);
})}
{/* Exterior label */}
addLayer(setLayers)}>
+ Strat
R straturi
{r.toFixed(3)} m²K/W
+ Rsi ({rsi}) + Rse ({rse})
{rTotal.toFixed(3)} m²K/W
U = 1/R total
{u.toFixed(3)} W/m²K
);
const diff = uA > 0 && uB > 0 ? ((uA - uB) / uA) * 100 : 0;
const betterSide = uA < uB ? "A" : uA > uB ? "B" : null;
return (
Compară două alcătuiri — prima de sus în jos (interior → exterior).
Trage straturile cu mouse-ul pentru reordonare sau folosește săgețile
↑↓. Dropdown-ul „Șablon" încarcă sisteme prestabilite.
{renderComposition("Alcătuire A", layersA, setLayersA, rA, rTotalA, uA)}
{renderComposition("Alcătuire B", layersB, setLayersB, rB, rTotalB, uB)}
{betterSide && (
Verdict: Alcătuirea{" "}
{betterSide} izolează mai bine cu{" "}
{Math.abs(diff).toFixed(1)}% (U
{betterSide === "A" ? "A" : "B"} ={" "}
{(betterSide === "A" ? uA : uB).toFixed(3)} vs U
{betterSide === "A" ? "B" : "A"} ={" "}
{(betterSide === "A" ? uB : uA).toFixed(3)} W/m²K).
{(betterSide === "A" ? uA : uB) <= 0.28 && (
Îndeplinește cerința C107 pentru perete exterior (U ≤ 0.28 W/m²K).
)}
)}
);
}
// ─── 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, "");
// Remove emoji
r = r.replace(/\p{Extended_Pictographic}/gu, "");
r = r.replace(/[\u{1F1E0}-\u{1F1FF}]/gu, ""); // flag emoji
r = r.replace(/[\u{FE00}-\u{FE0F}\u{20D0}-\u{20FF}]/gu, ""); // variation selectors
// 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 (
Text original (output AI)
Text curățat
{cleaned && }
Operații: eliminare markdown (###, **, `, liste, citate), emoji,
corectare encoding românesc (mojibake), curățare Unicode invizibil,
normalizare ghilimele / cratime / spații multiple.
);
}
// ─── MDLPA Date Locale ────────────────────────────────────────────────────────
function MdlpaValidator() {
return (
Portalul MDLPA nu permite încadrarea în iframe (blochează via
X-Frame-Options). Folosește linkurile de mai jos pentru a accesa direct
portalul.
);
}
// ─── PDF Reducer (Stirling PDF) ───────────────────────────────────────────────
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
function PdfReducer() {
const [file, setFile] = useState(null);
const [mode, setMode] = useState<"extreme" | "max" | "balanced">("extreme");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [result, setResult] = useState<{
originalSize: number;
compressedSize: number;
} | null>(null);
const [dragging, setDragging] = useState(false);
const fileRef = useRef(null);
const setFileFromInput = (f: File | null) => {
setFile(f);
setError("");
setResult(null);
};
const handleCompress = async () => {
if (!file) return;
setLoading(true);
setError("");
setResult(null);
try {
const formData = new FormData();
formData.append("fileInput", file);
// All modes use the GS endpoint with a level parameter
formData.append("level", mode === "extreme" ? "extreme" : mode === "max" ? "high" : "balanced");
const res = await fetch("/api/compress-pdf/extreme", {
method: "POST",
body: formData,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error ?? `Eroare server: ${res.status}`);
}
const blob = await res.blob();
const origHeader = res.headers.get("X-Original-Size");
const compHeader = res.headers.get("X-Compressed-Size");
setResult({
originalSize: origHeader ? Number(origHeader) : file.size,
compressedSize: compHeader ? Number(compHeader) : blob.size,
});
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 (err) {
setError(
err instanceof Error
? err.message
: "Nu s-a putut contacta Stirling PDF.",
);
} finally {
setLoading(false);
}
};
return (
Nivel compresie
setMode(e.target.value as "extreme" | "max" | "balanced")
}
className="mt-1 w-full rounded-md border bg-background px-3 py-2 text-sm"
>
Extremă — imagini 100 DPI, calitate scăzută
Puternică — imagini 150 DPI, calitate medie (recomandat)
Echilibrată — imagini 200 DPI, calitate bună
{mode === "extreme"
? "Reduce maxim dimensiunea. Imaginile pot pierde detalii fine."
: mode === "max"
? "Balanță bună între dimensiune și calitate. Recomandat pentru majoritatea fișierelor."
: "Pierdere minimă de calitate. Ideal pentru documente cu grafice detaliate."}
{loading ? "Se comprimă..." : "Comprimă PDF"}
{result && (
{formatBytes(result.originalSize)} →{" "}
{formatBytes(result.compressedSize)}
−
{(
((result.originalSize - result.compressedSize) /
result.originalSize) *
100
).toFixed(0)}
% mai mic
)}
{error &&
{error}
}
);
}
// ─── PDF Unlock (Stirling PDF) ────────────────────────────────────────────────
function PdfUnlock() {
const [file, setFile] = useState(null);
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [dragging, setDragging] = useState(false);
const fileRef = useRef(null);
const setFileFromInput = (f: File | null) => {
setFile(f);
setError("");
};
const handleUnlock = async () => {
if (!file) return;
setLoading(true);
setError("");
try {
const formData = new FormData();
formData.append("fileInput", file);
formData.append("password", password);
const res = await fetch("/api/compress-pdf/unlock", {
method: "POST",
body: formData,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.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, "-deblocat.pdf");
a.click();
URL.revokeObjectURL(url);
} catch (err) {
setError(
err instanceof Error ? err.message : "Nu s-a putut debloca PDF-ul.",
);
} finally {
setLoading(false);
}
};
return (
Parolă (dacă e criptat cu parolă)
setPassword(e.target.value)}
placeholder="Lasă gol dacă nu are parolă"
className="mt-1"
/>
{loading ? "Se deblochează..." : "Deblochează PDF"}
{error &&
{error}
}
);
}
// ─── DWG → DXF Converter ─────────────────────────────────────────────────────
function DwgToDxf() {
const [file, setFile] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [dragging, setDragging] = useState(false);
const fileRef = useRef(null);
const setFileFromInput = (f: File | null) => {
setFile(f);
setError("");
};
const handleConvert = async () => {
if (!file) return;
setLoading(true);
setError("");
try {
const formData = new FormData();
formData.append("fileInput", file);
const res = await fetch("/api/dwg-convert", {
method: "POST",
body: formData,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.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(/\.dwg$/i, ".dxf");
a.click();
URL.revokeObjectURL(url);
} catch (err) {
setError(
err instanceof Error ? err.message : "Nu s-a putut converti fișierul.",
);
} finally {
setLoading(false);
}
};
return (
Convertește AutoCAD DWG în format DXF deschis. Suportă versiuni până la
DWG 2018. Conversie server-side via libredwg.
{loading ? "Se convertește..." : "Convertește în DXF"}
{error &&
{error}
}
);
}
// ─── Quick OCR ────────────────────────────────────────────────────────────────
function QuickOcr() {
const [imageSrc, setImageSrc] = useState(null);
const [text, setText] = useState("");
const [progress, setProgress] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [lang, setLang] = useState("ron+eng");
const fileRef = useRef(null);
const runOcr = async (src: string) => {
if (loading) return;
setLoading(true);
setError("");
setText("");
setProgress(0);
try {
const { createWorker } = await import("tesseract.js");
const worker = await createWorker(lang.split("+"), 1, {
logger: (m: { status: string; progress: number }) => {
if (m.status === "recognizing text")
setProgress(Math.round(m.progress * 100));
},
});
const { data } = await worker.recognize(src);
setText(data.text.trim());
await worker.terminate();
} catch (e) {
setError(e instanceof Error ? e.message : "Eroare OCR necunoscută");
} finally {
setLoading(false);
}
};
const handleFile = (file: File) => {
const reader = new FileReader();
reader.onload = (e) => {
const src = e.target?.result as string;
setImageSrc(src);
runOcr(src);
};
reader.readAsDataURL(file);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const file = Array.from(e.dataTransfer.files).find((f) =>
f.type.startsWith("image/"),
);
if (file) handleFile(file);
};
const handlePaste = (e: React.ClipboardEvent) => {
const item = Array.from(e.clipboardData.items).find((i) =>
i.type.startsWith("image/"),
);
const file = item?.getAsFile();
if (file) handleFile(file);
};
return (
setLang(e.target.value)}
className="rounded-md border bg-background px-3 py-1.5 text-sm"
>
Română + Engleză
Română
Engleză
sau Ctrl+V pentru a lipi imaginea
fileRef.current?.click()}
onDrop={handleDrop}
onDragOver={(e) => e.preventDefault()}
>
{imageSrc ? (
// eslint-disable-next-line @next/next/no-img-element
) : (
Trage o imagine aici, apasă pentru a selecta, sau Ctrl+V
)}
{
const f = e.target.files?.[0];
if (f) handleFile(f);
}}
/>
{loading && (
Se procesează... (primul rulaj descarcă modelul ~10 MB)
{progress}%
)}
{error &&
{error}
}
{text && (
)}
);
}
// ─── Number to Text (Romanian) ────────────────────────────────────────────────
const ONES = [
"",
"unu",
"doi",
"trei",
"patru",
"cinci",
"șase",
"șapte",
"opt",
"nouă",
"zece",
"unsprezece",
"doisprezece",
"treisprezece",
"paisprezece",
"cincisprezece",
"șaisprezece",
"șaptesprezece",
"optsprezece",
"nouăsprezece",
];
const TENS = [
"",
"zece",
"douăzeci",
"treizeci",
"patruzeci",
"cincizeci",
"șaizeci",
"șaptezeci",
"optzeci",
"nouăzeci",
];
function numberToRoText(n: number): string {
if (n === 0) return "zero";
if (n < 0) return "minus " + numberToRoText(-n);
const parts: string[] = [];
// Billions
const mld = Math.floor(n / 1_000_000_000);
if (mld > 0) {
if (mld === 1) parts.push("un miliard");
else if (mld < 20) parts.push(ONES[mld]! + " miliarde");
else parts.push(twoDigitRo(mld) + " de miliarde");
n %= 1_000_000_000;
}
// Millions
const mil = Math.floor(n / 1_000_000);
if (mil > 0) {
if (mil === 1) parts.push("un milion");
else if (mil < 20) parts.push(ONES[mil]! + " milioane");
else parts.push(twoDigitRo(mil) + " de milioane");
n %= 1_000_000;
}
// Thousands
const th = Math.floor(n / 1000);
if (th > 0) {
if (th === 1) parts.push("o mie");
else if (th === 2) parts.push("două mii");
else if (th < 20) parts.push(ONES[th]! + " mii");
else if (th < 100) parts.push(twoDigitRo(th) + " de mii");
else parts.push(threeDigitRo(th) + " de mii");
n %= 1000;
}
// Hundreds
const h = Math.floor(n / 100);
if (h > 0) {
if (h === 1) parts.push("o sută");
else if (h === 2) parts.push("două sute");
else parts.push(ONES[h]! + " sute");
n %= 100;
}
// Remainder < 100
if (n > 0) {
if (parts.length > 0) parts.push("și");
if (n < 20) {
parts.push(ONES[n]!);
} else {
parts.push(twoDigitRo(n));
}
}
return parts.join(" ");
}
function twoDigitRo(n: number): string {
if (n < 20) return ONES[n]!;
const t = Math.floor(n / 10);
const o = n % 10;
if (o === 0) return TENS[t]!;
return TENS[t]! + " și " + ONES[o]!;
}
function threeDigitRo(n: number): string {
const h = Math.floor(n / 100);
const rest = n % 100;
let s = "";
if (h === 1) s = "o sută";
else if (h === 2) s = "două sute";
else s = ONES[h]! + " sute";
if (rest > 0) s += " " + twoDigitRo(rest);
return s;
}
function NumberToText() {
const [input, setInput] = useState("");
const v = parseFloat(input);
const isValid = !isNaN(v) && isFinite(v) && v >= 0 && v < 1e12;
const intPart = isValid ? Math.floor(v) : 0;
const decStr = input.includes(".") ? (input.split(".")[1] ?? "") : "";
const decPart = decStr ? parseInt(decStr.slice(0, 2).padEnd(2, "0"), 10) : 0;
const intText = isValid ? numberToRoText(intPart) : "";
const decText = decPart > 0 ? numberToRoText(decPart) : "";
// lei + bani version
const leiBani = isValid
? intText + " lei" + (decPart > 0 ? " și " + decText + " de bani" : "")
: "";
// Compact (no spaces between groups)
const compact = isValid
? intText.replace(/ și /g, "șI").replace(/ /g, "").replace(/șI/g, "și") +
(decPart > 0
? " virgulă " +
decText.replace(/ și /g, "șI").replace(/ /g, "").replace(/șI/g, "și")
: "")
: "";
// Normal
const normal = isValid
? intText + (decPart > 0 ? " virgulă " + decText : "")
: "";
return (
Sumă (număr)
setInput(e.target.value)}
className="mt-1"
placeholder="ex: 432.20"
/>
{isValid && intText && (
Compact (fără spații)
{compact}
)}
);
}
// ─── Color Palette Extractor ──────────────────────────────────────────────────
function ColorPaletteExtractor() {
const [imageSrc, setImageSrc] = useState(null);
const [colors, setColors] = useState([]);
const [copiedIdx, setCopiedIdx] = useState(null);
const fileRef = useRef(null);
const extractColors = useCallback((src: string) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
const canvas = document.createElement("canvas");
const size = 100;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0, size, size);
const data = ctx.getImageData(0, 0, size, size).data;
// Simple K-means-ish clustering: bucket colors
const buckets = new Map<
string,
{ r: number; g: number; b: number; count: number }
>();
for (let i = 0; i < data.length; i += 4) {
const r = Math.round(data[i]! / 32) * 32;
const g = Math.round(data[i + 1]! / 32) * 32;
const b = Math.round(data[i + 2]! / 32) * 32;
const key = `${r},${g},${b}`;
const existing = buckets.get(key);
if (existing) {
existing.r += data[i]!;
existing.g += data[i + 1]!;
existing.b += data[i + 2]!;
existing.count++;
} else {
buckets.set(key, {
r: data[i]!,
g: data[i + 1]!,
b: data[i + 2]!,
count: 1,
});
}
}
const sorted = Array.from(buckets.values())
.sort((a, b) => b.count - a.count)
.slice(0, 8);
const palette = sorted.map((b) => {
const r = Math.round(b.r / b.count);
const g = Math.round(b.g / b.count);
const bl = Math.round(b.b / b.count);
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bl.toString(16).padStart(2, "0")}`;
});
setColors(palette);
};
img.src = src;
}, []);
const handleFile = (file: File) => {
if (!file.type.startsWith("image/")) return;
const reader = new FileReader();
reader.onload = (e) => {
const src = e.target?.result as string;
setImageSrc(src);
extractColors(src);
};
reader.readAsDataURL(file);
};
const copyColor = async (hex: string, idx: number) => {
try {
await navigator.clipboard.writeText(hex.toUpperCase());
setCopiedIdx(idx);
setTimeout(() => setCopiedIdx(null), 1500);
} catch {
/* silent */
}
};
return (
fileRef.current?.click()}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
const f = e.dataTransfer.files[0];
if (f) handleFile(f);
}}
onPaste={(e) => {
const f = e.clipboardData.files[0];
if (f) handleFile(f);
// Also support pasting image data from clipboard
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item?.type.startsWith("image/")) {
const blob = item.getAsFile();
if (blob) handleFile(blob);
break;
}
}
}}
>
{imageSrc ? (
// eslint-disable-next-line @next/next/no-img-element
) : (
<>
Trage o imagine, apasă, sau lipește (Ctrl+V)
>
)}
{
const f = e.target.files?.[0];
if (f) handleFile(f);
}}
/>
{colors.length > 0 && (
Paletă extrasă ({colors.length} culori)
{colors.map((hex, i) => (
void copyColor(hex, i)}
title={`Click pentru a copia ${hex.toUpperCase()}`}
>
{copiedIdx === i ? "Copiat!" : hex.toUpperCase()}
))}
c.toUpperCase()).join(", ")} />
Copiază toate culorile
)}
);
}
// ─── Calculator Scară ─────────────────────────────────────────────────────────
const SCALE_PRESETS = [
{ label: "1:20", value: 20 },
{ label: "1:50", value: 50 },
{ label: "1:100", value: 100 },
{ label: "1:200", value: 200 },
{ label: "1:500", value: 500 },
{ label: "1:1000", value: 1000 },
{ label: "1:5000", value: 5000 },
];
// Real unit → multiplier to convert to mm
const REAL_UNITS = [
{ id: "mm", label: "mm", toMm: 1 },
{ id: "cm", label: "cm", toMm: 10 },
{ id: "m", label: "m", toMm: 1_000 },
{ id: "km", label: "km", toMm: 1_000_000 },
] as const;
type RealUnit = (typeof REAL_UNITS)[number]["id"];
/**
* Scale calculator.
*
* Core identity (all in mm):
* drawing_mm = real_mm / scale
* real_mm = drawing_mm × scale
*
* Real input in any unit → convert to mm → apply scale → drawing in mm.
* Drawing input in mm → apply scale → real mm → convert to chosen unit.
*/
function ScaleCalculator() {
const [scale, setScale] = useState(100);
const [customScale, setCustomScale] = useState("");
const [mode, setMode] = useState<"real-to-drawing" | "drawing-to-real">("real-to-drawing");
const [realUnit, setRealUnit] = useState("cm");
const [inputVal, setInputVal] = useState("");
const effectiveScale = customScale !== "" ? parseFloat(customScale) || scale : scale;
const val = parseFloat(inputVal);
const unitDef = REAL_UNITS.find((u) => u.id === realUnit)!;
// drawing_mm = real_in_unit × unitDef.toMm / scale
// real_in_unit = drawing_mm × scale / unitDef.toMm
const drawing_mm = !isNaN(val) && mode === "real-to-drawing"
? (val * unitDef.toMm) / effectiveScale
: NaN;
const real_in_unit = !isNaN(val) && mode === "drawing-to-real"
? (val * effectiveScale) / unitDef.toMm
: NaN;
// For secondary display: real_mm (whichever mode)
const real_mm = mode === "real-to-drawing"
? val * unitDef.toMm
: val * effectiveScale;
const fmt = (n: number, decimals = 2) =>
isNaN(n) || !isFinite(n)
? "—"
: n.toLocaleString("ro-RO", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
// Build secondary real conversions (exclude current unit)
const secondaryReal = (mm: number) =>
REAL_UNITS.filter((u) => u.id !== realUnit)
.map((u) => `${fmt(mm / u.toMm)} ${u.id}`)
.join(" · ");
return (
{/* Scale picker */}
Scară
{SCALE_PRESETS.map((p) => (
{ setScale(p.value); setCustomScale(""); }}
>
{p.label}
))}
1:
setCustomScale(e.target.value)}
placeholder="Altă scară"
className="w-28 text-sm"
/>
{/* Mode + real unit picker */}
{ setMode("real-to-drawing"); setInputVal(""); }}
>
Real → Desen
{ setMode("drawing-to-real"); setInputVal(""); }}
>
Desen → Real
Unitate reală:
{REAL_UNITS.map((u) => (
{ setRealUnit(u.id); setInputVal(""); }}
>
{u.label}
))}
{/* Input */}
{/* Result */}
{!isNaN(val) && val > 0 && (
Scară 1:{effectiveScale}
{mode === "real-to-drawing" ? (
<>
Real:{" "}
{fmt(val)} {realUnit}
{realUnit !== "mm" && (
({secondaryReal(real_mm)})
)}
Pe desen:{" "}
{fmt(drawing_mm)} mm
{!isNaN(drawing_mm) && (
= {fmt(drawing_mm / 10)} cm pe desen
)}
>
) : (
<>
Pe desen: {fmt(val)} mm
Real:{" "}
{fmt(real_in_unit)} {realUnit}
{!isNaN(real_mm) && (
{secondaryReal(real_mm)}
)}
>
)}
)}
);
}
// ─── Main Module ──────────────────────────────────────────────────────────────
export function MiniUtilitiesModule() {
return (
{/* ── Text & Calcule ── */}
Text
Caractere
Nr→Text
Procente
TVA
Suprafețe
U↔R
Termică
MDLPA
Scară
{/* ── Documente & Unelte ── */}
Compresie PDF
Deblocare PDF
DWG→DXF
OCR
Curățare AI
Culori
Transformare text
Numărare caractere
Calculator procente
Calculator TVA
Convertor suprafețe
Convertor U ↔ R (termoizolație)
Comparație termică materiale
Curățare text AI
MDLPA — Date locale construcții
Reducere dimensiune PDF
Deblocare PDF
Convertor DWG → DXF
OCR — extragere text din imagini
Transformare numere în litere
Extractor paletă culori
Calculator scară desen
);
}