diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index b77333d..5bd7b63 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -214,9 +214,9 @@ const Page = ({ params: { slug } }: { params: { slug: string } }) => { {/* Tools Panel - 20% width, fixed */} {development_tools_list?.length > 0 && (
-
+
-

+

Other Tools

diff --git a/app/components/developmentToolsComponent/imageResizer.tsx b/app/components/developmentToolsComponent/imageResizer.tsx new file mode 100644 index 0000000..b8d35a0 --- /dev/null +++ b/app/components/developmentToolsComponent/imageResizer.tsx @@ -0,0 +1,587 @@ +"use client"; + +import React, { useEffect, useMemo, useRef, useState } from "react"; +import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss"; + +const clamp = (value: number, min: number, max: number) => { + return Math.min(max, Math.max(min, value)); +}; + +type LoadedImage = { + img: HTMLImageElement; + width: number; + height: number; +}; + +type OutputFormat = "png" | "jpg" | "webp"; +type SizeUnit = "KB" | "MB"; + +const readFileAsDataURL = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(String(reader.result)); + reader.onerror = reject; + reader.readAsDataURL(file); + }); +}; + +const ImageResizer = () => { + const canvasRef = useRef(null); + + const [src, setSrc] = useState(""); + const [image, setImage] = useState(null); + const [fileName, setFileName] = useState(""); + const [error, setError] = useState(""); + const [isDragging, setIsDragging] = useState(false); + + const [keepAspectRatio, setKeepAspectRatio] = useState(true); + const [targetWidth, setTargetWidth] = useState(0); + const [targetHeight, setTargetHeight] = useState(0); + + const [format, setFormat] = useState("png"); + const [quality, setQuality] = useState(92); // for jpg/webp + const [transparent, setTransparent] = useState(true); // for jpg, affects bg fill + const [bg, setBg] = useState("#ffffff"); + + const [output, setOutput] = useState(""); + const [outputBytes, setOutputBytes] = useState(0); + const [targetSize, setTargetSize] = useState(300); + const [targetUnit, setTargetUnit] = useState("KB"); + const [isOptimizing, setIsOptimizing] = useState(false); + + const aspect = useMemo(() => { + if (!image) return 1; + return image.width / Math.max(1, image.height); + }, [image]); + + useEffect(() => { + if (!src) return setImage(null); + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + setImage({ img, width: img.width, height: img.height }); + }; + img.onerror = () => setError("Failed to load image"); + img.src = src; + }, [src]); + + useEffect(() => { + if (!image) return; + setTargetWidth(image.width); + setTargetHeight(image.height); + }, [image]); + + const mime = useMemo(() => { + if (format === "jpg") return "image/jpeg"; + if (format === "webp") return "image/webp"; + return "image/png"; + }, [format]); + + const normalizedQuality = useMemo(() => { + if (format === "png") return undefined; + return clamp(quality / 100, 0.1, 1); + }, [format, quality]); + + const renderToBlob = (qualityOverride?: number): Promise => { + if (!image) return Promise.resolve(null); + const canvas = canvasRef.current; + if (!canvas) return Promise.resolve(null); + + const w = clamp(Math.floor(targetWidth || 0), 1, 20000); + const h = clamp(Math.floor(targetHeight || 0), 1, 20000); + + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + if (!ctx) return Promise.resolve(null); + + ctx.clearRect(0, 0, w, h); + if (format === "jpg" && !transparent) { + ctx.fillStyle = bg; + ctx.fillRect(0, 0, w, h); + } + + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = "high"; + ctx.drawImage(image.img, 0, 0, w, h); + + const q = + format === "png" + ? undefined + : typeof qualityOverride === "number" + ? clamp(qualityOverride, 0.1, 1) + : normalizedQuality; + + return new Promise((resolve) => { + canvas.toBlob( + (blob) => resolve(blob), + mime, + q + ); + }); + }; + + const draw = async () => { + if (!image) return; + const blob = await renderToBlob(); + if (!blob) return; + setOutput((prev) => { + if (prev) URL.revokeObjectURL(prev); + return URL.createObjectURL(blob); + }); + setOutputBytes(blob.size); + }; + + useEffect(() => { + draw(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [image, targetWidth, targetHeight, format, quality, transparent, bg]); + + useEffect(() => { + return () => { + if (output) URL.revokeObjectURL(output); + }; + }, [output]); + + const onFile = async (file?: File) => { + if (!file) return; + setError(""); + try { + const dataUrl = await readFileAsDataURL(file); + setSrc(dataUrl); + setFileName(file.name); + } catch { + setError("Failed to read file"); + } + }; + + const setWidthAndMaybeHeight = (w: number) => { + const newW = clamp(Math.floor(w || 0), 1, 20000); + setTargetWidth(newW); + if (keepAspectRatio && image) { + setTargetHeight(Math.max(1, Math.round(newW / aspect))); + } + }; + + const setHeightAndMaybeWidth = (h: number) => { + const newH = clamp(Math.floor(h || 0), 1, 20000); + setTargetHeight(newH); + if (keepAspectRatio && image) { + setTargetWidth(Math.max(1, Math.round(newH * aspect))); + } + }; + + const download = () => { + if (!output) return; + const a = document.createElement("a"); + a.href = output; + const base = fileName ? fileName.replace(/\.[^.]+$/, "") : "image"; + a.download = `${base}-${targetWidth}x${targetHeight}.${format === "jpg" ? "jpg" : format}`; + a.click(); + }; + + const targetBytes = useMemo(() => { + const n = Math.max(1, Number(targetSize) || 0); + return targetUnit === "MB" ? Math.round(n * 1024 * 1024) : Math.round(n * 1024); + }, [targetSize, targetUnit]); + + const compressToTargetSize = async () => { + if (!image) return; + if (format === "png") return; + setError(""); + setIsOptimizing(true); + try { + const maxIters = 9; + let lo = 0.1; + let hi = 1; + let bestBlob: Blob | null = null; + let bestQ = hi; + + for (let i = 0; i < maxIters; i++) { + const mid = (lo + hi) / 2; + const blob = await renderToBlob(mid); + if (!blob) break; + + if (blob.size <= targetBytes) { + bestBlob = blob; + bestQ = mid; + lo = mid; + } else { + hi = mid; + } + } + + const finalBlob = bestBlob ?? (await renderToBlob(0.1)); + if (!finalBlob) return; + + setQuality(Math.round(clamp(bestQ, 0.1, 1) * 100)); + setOutput((prev) => { + if (prev) URL.revokeObjectURL(prev); + return URL.createObjectURL(finalBlob); + }); + setOutputBytes(finalBlob.size); + } catch { + setError("Failed to optimize file size"); + } finally { + setIsOptimizing(false); + } + }; + + const clearAll = () => { + if (output) URL.revokeObjectURL(output); + setSrc(""); + setImage(null); + setFileName(""); + setError(""); + setKeepAspectRatio(true); + setTargetWidth(0); + setTargetHeight(0); + setFormat("png"); + setQuality(92); + setTransparent(true); + setBg("#ffffff"); + setOutput(""); + setOutputBytes(0); + setTargetSize(300); + setTargetUnit("KB"); + setIsOptimizing(false); + }; + + return ( +
+
+
+
+
+
+
+
+
+
+
+
Upload
+
Drag & drop or pick a file
+
+ {image && ( +
+ {image.width}×{image.height}px +
+ )} +
+ +
{ + e.preventDefault(); + setIsDragging(true); + }} + onDragEnter={() => setIsDragging(true)} + onDragLeave={() => setIsDragging(false)} + onDrop={(e) => { + e.preventDefault(); + setIsDragging(false); + onFile(e.dataTransfer.files?.[0]); + }} + className={`rounded-xl border border-dashed ${isDragging ? "border-primary bg-black/60" : "border-[#222222] bg-black/40" + } p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 transition-colors`} + > +
+
+ {fileName || "Drop an image here…"} +
+
We never upload your file.
+
+
+ {src && ( + + )} + +
+
+
+ +
+
+
Resize
+
Set output dimensions in pixels
+
+ +
+
+ + setWidthAndMaybeHeight(Number(e.target.value))} + min={1} + max={20000} + disabled={!image} + className="w-full bg-black border border-[#222222] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-primary disabled:opacity-60" + placeholder={image ? String(image.width) : "—"} + /> +
+ +
+ + setHeightAndMaybeWidth(Number(e.target.value))} + min={1} + max={20000} + disabled={!image} + className="w-full bg-black border border-[#222222] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-primary disabled:opacity-60" + placeholder={image ? String(image.height) : "—"} + /> +
+
+ +
+
+ +
Locks width/height together.
+
+ setKeepAspectRatio(e.target.checked)} + disabled={!image} + /> +
+
+ +
+
+
Export
+
Choose format and compression
+
+ +
+ + + +
+ + {format !== "png" && ( +
+
+ Quality + {quality} +
+ setQuality(Number(e.target.value))} + style={{ accentColor: "#00DA92" }} + className="w-full mt-2" + /> +
+ )} + +
+
+
+
Target file size
+
+ Works for JPG/WebP (PNG is lossless). +
+
+
+
+ setTargetSize(Number(e.target.value))} + className="col-span-2 w-full bg-black border border-[#222222] rounded-lg px-3 py-2 text-white focus:outline-none focus:border-primary" + disabled={!image || format === "png"} + /> + +
+ +
+ + {format === "jpg" && ( +
+
+
+
Background
+
JPG has no transparency.
+
+
+ setTransparent(!e.target.checked)} + /> + +
+
+ +
+ setBg(e.target.value)} + className="w-9 h-9 rounded-md border border-[#222222]" + disabled={transparent} + /> +
+ {["#ffffff", "#000000", "#f43f5e", "#f59e0b", "#10b981", "#3b82f6"].map((c) => ( +
+
+
+ )} + +
+ + +
+
+
+ +
+
+
+
+
Preview
+
Canvas render of your export
+
+ {image && ( +
+ Output: {targetWidth}×{targetHeight}px +
+ )} +
+ +
+ {/* Keep canvas mounted so `draw()` can always produce output */} + + {output ? ( + Resized + ) : ( +
Upload an image to preview the result
+ )} +
+ {output ? ( +
+
+ Size: {(outputBytes / 1024).toFixed(1)} KB +
+ {format !== "png" && ( +
+ Quality: {quality} +
+ )} +
+ ) : null} +
+
+
+ + {error && ( +
+
{error}
+
+ )} +
+
+
+
+
+
+ ); +}; + +export default ImageResizer; + diff --git a/app/components/developmentToolsComponent/timeCalculator.tsx b/app/components/developmentToolsComponent/timeCalculator.tsx new file mode 100644 index 0000000..3c23cf9 --- /dev/null +++ b/app/components/developmentToolsComponent/timeCalculator.tsx @@ -0,0 +1,425 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +import DevelopmentToolsStyles from "../../developmentToolsStyles.module.scss"; + +type Mode = "add" | "subtract" | "multiply" | "divide" | "between"; +type Unit = "ms" | "s" | "min" | "h" | "d" | "w" | "mo" | "y"; + +const UNIT_OPTIONS: { value: Unit; label: string }[] = [ + { value: "ms", label: "ms" }, + { value: "s", label: "seconds" }, + { value: "min", label: "minutes" }, + { value: "h", label: "hours" }, + { value: "d", label: "days" }, + { value: "w", label: "weeks" }, + { value: "mo", label: "months*" }, + { value: "y", label: "years*" }, +]; + +const UNIT_TO_MS: Record = { + ms: 1, + s: 1000, + min: 60_000, + h: 3_600_000, + d: 86_400_000, + w: 604_800_000, + // Omni-style approximations + mo: 30.5 * 86_400_000, + y: 365 * 86_400_000, +}; + +const pad2 = (n: number) => String(n).padStart(2, "0"); + +const formatPretty = (totalMs: number) => { + const sign = totalMs < 0 ? "-" : ""; + const ms = Math.abs(Math.trunc(totalMs)); + const totalSeconds = Math.floor(ms / 1000); + const seconds = totalSeconds % 60; + const totalMinutes = Math.floor(totalSeconds / 60); + const minutes = totalMinutes % 60; + const hours = Math.floor(totalMinutes / 60); + return `${sign}${hours} h ${minutes} min ${seconds} sec`; +}; + +const breakdown = (totalMs: number) => { + const sign = totalMs < 0 ? -1 : 1; + let ms = Math.abs(Math.trunc(totalMs)); + const days = Math.floor(ms / UNIT_TO_MS.d); + ms -= days * UNIT_TO_MS.d; + const hours = Math.floor(ms / UNIT_TO_MS.h); + ms -= hours * UNIT_TO_MS.h; + const minutes = Math.floor(ms / UNIT_TO_MS.min); + ms -= minutes * UNIT_TO_MS.min; + const seconds = Math.floor(ms / UNIT_TO_MS.s); + ms -= seconds * UNIT_TO_MS.s; + return { sign, days, hours, minutes, seconds, milliseconds: ms }; +}; + +type HMSRow = { h: number; m: number; s: number }; + +const DEFAULT_HMS_ROWS: HMSRow[] = [ + { h: 0, m: 0, s: 0 }, + { h: 0, m: 0, s: 0 }, +]; + +const toMs = (r: HMSRow) => (Number(r.h) * 3600 + Number(r.m) * 60 + Number(r.s)) * 1000; + +const calc = ( + mode: Mode, + rows: HMSRow[], + multiplier: number, + divisor: number, + betweenStart: string, + betweenEnd: string +) => { + const total = rows.reduce((acc, r) => acc + toMs(r), 0); + + if (mode === "add") return { ms: total, error: "" }; + + if (mode === "subtract") { + if (rows.length < 2) return { ms: total, error: "" }; + const first = toMs(rows[0] ?? { h: 0, m: 0, s: 0 }); + const rest = rows.slice(1).reduce((acc, r) => acc + toMs(r), 0); + return { ms: first - rest, error: "" }; + } + + if (mode === "multiply") return { ms: total * Number(multiplier || 0), error: "" }; + + if (mode === "divide") { + const d = Number(divisor || 0); + if (d === 0) return { ms: 0, error: "Divisor cannot be 0." }; + return { ms: total / d, error: "" }; + } + + // between + if (!betweenStart || !betweenEnd) return { ms: 0, error: "" }; + const a = new Date(betweenStart).getTime(); + const b = new Date(betweenEnd).getTime(); + if (!Number.isFinite(a) || !Number.isFinite(b)) { + return { ms: 0, error: "Please select valid start and end dates." }; + } + return { ms: b - a, error: "" }; +}; + +const TimeCalculator = () => { + const [mode, setMode] = useState("add"); + const [rows, setRows] = useState(DEFAULT_HMS_ROWS); + const [resultUnit, setResultUnit] = useState("h"); + const [multiplier, setMultiplier] = useState(2); + const [divisor, setDivisor] = useState(2); + const [betweenStart, setBetweenStart] = useState(""); + const [betweenEnd, setBetweenEnd] = useState(""); + + const computed = useMemo( + () => calc(mode, rows, multiplier, divisor, betweenStart, betweenEnd), + [mode, rows, multiplier, divisor, betweenStart, betweenEnd] + ); + + const resultMs = computed.ms; + const error = computed.error; + + const resultInUnit = useMemo(() => { + const denom = UNIT_TO_MS[resultUnit]; + if (!denom) return 0; + return resultMs / denom; + }, [resultMs, resultUnit]); + + const pretty = useMemo(() => formatPretty(resultMs), [resultMs]); + const parts = useMemo(() => breakdown(resultMs), [resultMs]); + + const addRow = () => setRows((prev) => [...prev, { h: 0, m: 0, s: 0 }]); + const removeRow = (idx: number) => setRows((prev) => prev.filter((_, i) => i !== idx)); + const updateRow = (idx: number, patch: Partial) => + setRows((prev) => prev.map((r, i) => (i === idx ? { ...r, ...patch } : r))); + + const clearAll = () => { + setRows(DEFAULT_HMS_ROWS); + setResultUnit("h"); + setMultiplier(2); + setDivisor(2); + setBetweenStart(""); + setBetweenEnd(""); + setMode("add"); + }; + + const copy = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch { + // ignore + } + }; + + const modeTitle = + mode === "add" + ? "Add time" + : mode === "subtract" + ? "Subtract time" + : mode === "multiply" + ? "Multiply time" + : mode === "divide" + ? "Divide time" + : "Time between dates"; + + return ( +
+
+
+
+
+
+
+
+
+
+
+
Calculator
+
{modeTitle}
+
+ +
+ +
+ + +
+ + {mode === "between" ? ( +
+
+ + setBetweenStart(e.target.value)} + className="w-full bg-black border border-[#222222] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-primary" + /> +
+
+ + setBetweenEnd(e.target.value)} + className="w-full bg-black border border-[#222222] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-primary" + /> +
+
+ ) : ( +
+
Enter values (up to 20 rows)
+
3 ? "bb-thin-scroll max-h-[420px] overflow-auto pr-1" : "" + }`} + style={rows.length > 3 ? { scrollbarWidth: "thin" } : undefined} + > + {rows.slice(0, 20).map((r, idx) => ( +
+
+
+ {idx === 0 ? "Time 1" : idx === 1 ? "Time 2" : `Time ${idx + 1}`} +
+ +
+ +
+
+
+
hrs
+ updateRow(idx, { h: Number(e.target.value) || 0 })} + className="w-full bg-transparent outline-none text-white text-sm" + /> +
+
+
min
+ updateRow(idx, { m: Number(e.target.value) || 0 })} + className="w-full bg-transparent outline-none text-white text-sm" + /> +
+
+
sec
+ updateRow(idx, { s: Number(e.target.value) || 0 })} + className="w-full bg-transparent outline-none text-white text-sm" + /> +
+
+
+
+ ))} +
+ +
+ + + {mode === "multiply" && ( +
+ × + setMultiplier(Number(e.target.value) || 0)} + className="w-28 bg-black border border-[#222222] rounded-lg px-3 py-2 text-white focus:outline-none focus:border-primary" + /> +
+ )} + + {mode === "divide" && ( +
+ ÷ + setDivisor(Number(e.target.value) || 0)} + className="w-28 bg-black border border-[#222222] rounded-lg px-3 py-2 text-white focus:outline-none focus:border-primary" + /> +
+ )} +
+ +
+ Tip: Use multiple rows for add/subtract. Multiply/divide applies to the total. +
+
+ )} + + {error &&
{error}
} +
+
+ +
+
+
+
Result
+
View as formatted time and totals
+
+ +
+
Pretty
+
{pretty}
+
+ +
+
+
+
In units
+ +
+
+ {Number.isFinite(resultInUnit) ? resultInUnit.toFixed(6).replace(/\.?0+$/, "") : "—"} +
+
+ +
+
Breakdown
+
+ {parts.sign < 0 ? "-" : ""} + {parts.days}d {pad2(parts.hours)}h {pad2(parts.minutes)}m {pad2(parts.seconds)}s +
+
{parts.milliseconds} ms
+
+
+ +
+ + + +
+
+
+
+
+
+
+
+
+ +
+ ); +}; + +export default TimeCalculator; + diff --git a/app/developmentToolsStyles.module.scss b/app/developmentToolsStyles.module.scss index cb04ee0..eefe24c 100644 --- a/app/developmentToolsStyles.module.scss +++ b/app/developmentToolsStyles.module.scss @@ -1,4 +1,5 @@ @import "./styles/variables.scss"; + // serach box css .searchInput { display: flex; @@ -8,8 +9,6 @@ input { background: #ffffff1a; - border: 1px solid #ffffff66; - color: #ffffff; border-radius: 50px; padding: 2px 24px; height: 40px; @@ -22,6 +21,7 @@ &:focus { opacity: 1; + &::placeholder { opacity: 0%; } @@ -32,6 +32,7 @@ font-size: 16px; transition: all 0.3s linear; } + @media (max-width: 600px) { width: 370px; opacity: 1; @@ -39,163 +40,13 @@ padding: 2px 20px; padding-right: 95px; } + @media (min-width: 768px) { height: 50px; } } } -:global([data-theme="light"]) .pageContainer { - color: #111827; -} - -:global([data-theme="light"]) .contentWrapper { - color: #111827; -} - -:global([data-theme="light"]) .searchInput { - input { - background: #ffffff; - border: 1px solid #e5e7eb; - color: #111827; - opacity: 1; - - &::placeholder { - color: #6b7280; - } - } -} - -:global([data-theme="light"]) .promoPanel { - background: #f9fafb; - border: 1px solid #e5e7eb; - color: #111827; -} - -:global([data-theme="light"]) .filterSidebar { - background: #f9fafb; - border: 1px solid #e5e7eb; -} - -:global([data-theme="light"]) .favoriteButton { - background: #ffffff; - border-color: #e5e7eb; - color: #111827; - - &:hover { - background: #f3f4f6; - border-color: #d1d5db; - } - - &[data-active="true"] { - background: #e6f9f2; - border-color: #10b981; - color: #065f46; - } -} - -:global([data-theme="light"]) .favoriteCount { - color: #4b5563; -} - -:global([data-theme="light"]) .favoriteButton[data-active="true"] .favoriteCount { - color: #065f46; -} - -:global([data-theme="light"]) .filterHeading { - color: #111827; -} - -:global([data-theme="light"]) .filterSubLabel { - color: #4b5563; -} - -:global([data-theme="light"]) .filterCategoryButton { - background: #ffffff; - border-color: #e5e7eb; - color: #111827; - - &:hover { - background: #f6f7f8; - border-color: #d1d5db; - } - - &[data-active="true"] { - background: #e6f9f2; - border-color: #10b981; - color: #065f46; - } -} - -:global([data-theme="light"]) .filterBasisButton { - background: #ffffff; - border-color: #e5e7eb; - color: #111827; - - &:hover { - background: #f6f7f8; - border-color: #d1d5db; - } - - &[data-active="true"] { - background: #e6f9f2; - border-color: #10b981; - color: #065f46; - } -} - -:global([data-theme="light"]) .showingCount { - color: #4b5563; -} - -:global([data-theme="light"]) .activeFilterPill { - background: #f3f4f6; - border-color: #e5e7eb; - color: #111827; - - &:hover { - background: #e5e7eb; - } -} - -:global([data-theme="light"]) .sidePanel { - background: #f9fafb; - border: 1px solid #e5e7eb; - border-radius: 0.75rem; - color: #111827; - padding: 1rem; -} - -:global([data-theme="light"]) .sidePanelItem { - background: #ffffff; - border-color: #e5e7eb; - color: #111827; - - &:hover { - background: #f3f4f6; - border-color: #10b981; - } -} - -:global([data-theme="light"]) .toolCard { - background: #ffffff; - border: 1px solid #e5e7eb; -} - -:global([data-theme="light"]) .toolCardDescription { - color: #4b5563; -} - -:global([data-theme="light"]) .toolCardMeta { - color: #6b7280; -} - -:global([data-theme="light"]) .contentCardHoverEffect { - &:hover { - box-shadow: 2px 2px 4px rgba(16, 185, 129, 0.18); - } -} - // tab background color .tabBackgroundColor { background: linear-gradient(91.15deg, #11e498 11.3%, #05bae2 101.69%); @@ -213,13 +64,16 @@ // content card hover effect .contentCardHoverEffect { border-left: 2px solid #11e498; - div > p > svg { + + div>p>svg { color: #11e498; } + &:hover { - div > p > svg { + div>p>svg { color: black; } + background: linear-gradient(92.9deg, #00d1ff 23.92%, #16fca9 100.19%); transform: scale(0.4); -webkit-transform: scale(1.1); @@ -237,23 +91,22 @@ .converterButton { background: $primary; } + .clearButton { background: rgb(255, 79, 108); } + .copyButton { background: #00d0ff9a; } -.addToChromeButton { - background: #000000; - color: #ffffff; -} - /* Webkit custom scrollbar styles */ .scrollbar { overflow: auto; - scrollbar-width: thin; /* Firefox */ - scrollbar-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.05); /* Firefox */ + scrollbar-width: thin; + /* Firefox */ + scrollbar-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.05); + /* Firefox */ } /* Scrollbar track (background of the scrollbar) */ @@ -285,8 +138,10 @@ /* Modern scrollbar for code blocks */ .modernScrollbar { overflow: auto; - scrollbar-width: thin; /* Firefox */ - scrollbar-color: rgba(17, 228, 152, 0.4) rgba(255, 255, 255, 0.05); /* Firefox - using primary color */ + scrollbar-width: thin; + /* Firefox */ + scrollbar-color: rgba(17, 228, 152, 0.4) rgba(255, 255, 255, 0.05); + /* Firefox - using primary color */ } .modernScrollbar::-webkit-scrollbar { @@ -309,4 +164,4 @@ .modernScrollbar::-webkit-scrollbar-track { background-color: rgba(255, 255, 255, 0.05); border-radius: 10px; -} +} \ No newline at end of file diff --git a/app/libs/constants.tsx b/app/libs/constants.tsx index 5e558a0..2e38a8f 100644 --- a/app/libs/constants.tsx +++ b/app/libs/constants.tsx @@ -144,6 +144,7 @@ import RgbToCmykConverter from '../components/developmentToolsComponent/rgbToCmy import RgbToHexConverter from '../components/developmentToolsComponent/rgbToHexConverter'; import Rot13EncoderDecoderComponent from '../components/developmentToolsComponent/rot13EncoderDecoderComponent'; import RotateImageTool from '../components/developmentToolsComponent/rotateImageTool'; +import ImageResizer from '../components/developmentToolsComponent/imageResizer'; import RotationCalculatorComponent from '../components/developmentToolsComponent/rotationCalculatorComponent'; import ScssToCssConverter from '../components/developmentToolsComponent/scssToCssConverter'; import ShuffleLetters from '../components/developmentToolsComponent/shuffleLetters'; @@ -190,6 +191,7 @@ import XorCalculator from '../components/developmentToolsComponent/xorCalculator import CurlToCodeConverter from '../components/developmentToolsComponent/curlToCodeConverter'; import YAMLFormatterAndBeautifier from '../components/developmentToolsComponent/yamlFormatterAndBeautifier'; import EpochConverter from '../components/developmentToolsComponent/epochConverter'; +import TimeCalculator from '../components/developmentToolsComponent/timeCalculator'; export const WEB_URL = 'https://www.betterbugs.io'; @@ -1624,6 +1626,12 @@ export const developmentToolsCategoryContent: any = { title: 'Unix Timestamp Converter', description: 'Convert Unix timestamps to readable dates and vice versa.', }, + { + url: '/time-calculator', + title: 'Time Calculator', + description: + 'Add, subtract, multiply, divide time, or calculate the time between dates.', + }, ], Category179: [ { @@ -1633,6 +1641,19 @@ export const developmentToolsCategoryContent: any = { 'Decompose complex URLs into legible components and edit query parameters in a visual table.', }, ], + Category180: [ + { + url: '/url-encode', + title: 'URL Encode', + description: 'Encode URLs.', + }, + { + url: '/image-resizer', + title: 'Image Resizer', + description: + 'Resize images locally in your browser. Keep aspect ratio, choose format, and download.', + }, + ], }; export const PATHS = { @@ -1817,6 +1838,8 @@ export const PATHS = { MORSE_CODE_TRANSLATOR: '/morse-code-translator', CURL_TO_CODE_CONVERTER: '/curl-to-code-converter', UNIX_TIMESTAMP_CONVERTER: '/unix-timestamp-converter', + IMAGE_RESIZER: '/image-resizer', + TIME_CALCULATOR: '/time-calculator', }; export const developmentToolsRoutes = [ @@ -2075,6 +2098,10 @@ export const developmentToolsRoutes = [ path: PATHS.ROTATE_IMAGE_TOOL, component: , }, + { + path: PATHS.IMAGE_RESIZER, + component: , + }, { path: PATHS.CSV_TO_EXCEL_FILE_CONVERTOR, component: , @@ -2540,6 +2567,7 @@ export const developmentToolsRoutes = [ path: PATHS.UNIX_TIMESTAMP_CONVERTER, component: , }, + { path: PATHS.TIME_CALCULATOR, component: }, ]; // lorem ipsum text diff --git a/app/libs/developmentToolsConstant.tsx b/app/libs/developmentToolsConstant.tsx index 48a7d67..d80d449 100644 --- a/app/libs/developmentToolsConstant.tsx +++ b/app/libs/developmentToolsConstant.tsx @@ -11683,6 +11683,192 @@ console.log(encoded); // "SGVsbG8gV29ybGQh"`, og_image: '/images/og-images/Cover.png', }, }, + [`image-resizer`]: { + hero_section: { + title: 'Image Resizer', + description: + 'Resize images locally in your browser. Keep aspect ratio, choose output format (PNG/JPG/WebP), and download the resized file.', + }, + development_tools_list: [ + { tool: 'Rotate Image Tool', url: PATHS.ROTATE_IMAGE_TOOL }, + { tool: 'Placeholder Image Generator', url: PATHS.PLACEHOLDER_IMAGE_GENERATOR }, + { tool: 'Color Picker Tool', url: PATHS.COLOR_PICKER_TOOL }, + { tool: 'SVG to React/CSS Utility', url: PATHS.SVG_CONVERTER }, + { tool: 'CSS Minify', url: PATHS.CSS_MINIFY }, + { tool: 'URL Encode', url: PATHS.URL_ENCODE }, + ], + development_tools_about_details: { + about_title: 'What is the Image Resizer tool?', + about_description: [ + { + description: + 'The Image Resizer helps you change an image’s pixel dimensions (width and height) without uploading it to a server. Everything runs locally in your browser.', + }, + { + description: + 'Use it to prepare assets for websites, emails, social previews, or product listings, and to quickly generate smaller image variants for performance.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'How to use the Image Resizer', + guide_description: 'Resize an image in a few simple steps:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Upload an image', + step_description: + 'Drag and drop an image file (or choose a file) to load it into the resizer.', + }, + { + step_key: 'Step 2:', + step_title: 'Set the target size', + step_description: + 'Enter the width and height in pixels. Enable “Keep aspect ratio” to automatically calculate the other dimension.', + }, + { + step_key: 'Step 3:', + step_title: 'Pick output format and quality', + step_description: + 'Choose PNG, JPG, or WebP. For JPG/WebP, adjust quality to balance file size and clarity.', + }, + { + step_key: 'Step 4:', + step_title: 'Download the resized image', + step_description: + 'Preview the result and download the resized image with the selected settings.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'How It’s Used', + how_use_description: 'Common use cases for resizing images:', + point: [ + { + title: 'Preparing web assets', + description: + 'Resize hero images, thumbnails, and blog illustrations to the exact dimensions your layout needs.', + }, + { + title: 'Performance optimization', + description: + 'Create smaller variants to reduce page weight and improve Core Web Vitals (especially LCP).', + }, + { + title: 'Consistent content sizing', + description: + 'Standardize image sizes for product grids, cards, and social media previews.', + }, + { + title: 'Fast QA & debugging', + description: + 'Quickly generate test images at specific dimensions when validating responsive UI behavior.', + }, + ], + }, + meta_data: { + meta_title: 'Image Resizer – Resize Images Online (PNG/JPG/WebP)', + meta_description: + 'Resize images locally in your browser. Keep aspect ratio, choose PNG/JPG/WebP output, adjust quality, and download the resized image instantly.', + og_title: 'Image Resizer – Online Image Resize Tool', + og_description: + 'Resize images to exact pixel dimensions with optional aspect ratio lock. Export as PNG, JPG, or WebP and download immediately.', + og_image: '/images/og-images/Cover.png', + }, + }, + [`time-calculator`]: { + hero_section: { + title: 'Time Calculator', + description: + 'Add, subtract, multiply, divide time, or calculate the time between dates. Useful for planning tasks, estimating durations, and working with timestamps.', + }, + development_tools_list: [ + { tool: 'Unix Timestamp Converter', url: PATHS.UNIX_TIMESTAMP_CONVERTER }, + { tool: 'Random Date Generator', url: PATHS.RANDOM_DATE_GENERATOR }, + { tool: 'Random Time Generator', url: PATHS.RANDOM_CLOCK_TIME_GENERATOR }, + { tool: 'URL Parser & Query String Editor', url: PATHS.URL_PARSER }, + { tool: 'JSON Prettifier', url: PATHS.JSON_PRETTIFIER }, + { tool: 'Code Compare Tool', url: PATHS.CODE_COMPARE_TOOL }, + ], + development_tools_about_details: { + about_title: 'What is the Time Calculator?', + about_description: [ + { + description: + 'The Time Calculator helps you perform common operations on time values—adding, subtracting, multiplying, dividing, and finding the duration between dates.', + }, + { + description: + 'Use it for planning tasks, estimating project durations, tracking SLAs, or quickly converting totals into a readable time breakdown.', + }, + ], + }, + development_tools_steps_guide: { + guide_title: 'How to use the Time Calculator', + guide_description: 'Calculate time in a few steps:', + steps: [ + { + step_key: 'Step 1:', + step_title: 'Choose an operation', + step_description: + 'Select whether you want to add, subtract, multiply, divide time, or find the time between dates.', + }, + { + step_key: 'Step 2:', + step_title: 'Enter time values or dates', + step_description: + 'Add one or more rows of time values (hours, minutes, etc.), or choose start/end dates for a duration calculation.', + }, + { + step_key: 'Step 3:', + step_title: 'Adjust multipliers/units (optional)', + step_description: + 'For multiply/divide, set the multiplier or divisor. Choose a result unit to see totals in seconds, hours, days, and more.', + }, + { + step_key: 'Step 4:', + step_title: 'Copy the output', + step_description: + 'Copy a pretty formatted result, milliseconds, or seconds for use in docs, tickets, spreadsheets, or code.', + }, + ], + }, + development_tools_how_use: { + how_use_title: 'How It’s Used', + how_use_description: 'Common scenarios:', + point: [ + { + title: 'Planning & estimation', + description: + 'Add up multiple task durations to estimate total time required.', + }, + { + title: 'SLA / time windows', + description: + 'Compute allowed windows by subtracting or dividing time budgets.', + }, + { + title: 'Between two dates', + description: + 'Find the exact duration between a start and end timestamp.', + }, + { + title: 'Engineering & QA', + description: + 'Convert totals into readable breakdowns when debugging timers and intervals.', + }, + ], + }, + meta_data: { + meta_title: 'Time Calculator – Add, Subtract, Multiply & Divide Time', + meta_description: + 'A free time calculator to add, subtract, multiply, divide time, and calculate the time between dates. Get pretty results and copy seconds/milliseconds.', + og_title: 'Time Calculator – Online Tool', + og_description: + 'Calculate time instantly: add/subtract durations, multiply/divide time, or find the time between dates. Copy pretty output, seconds, or ms.', + og_image: '/images/og-images/Cover.png', + }, + }, [`text-to-html-entities-convertor`]: { hero_section: { title: 'Text to HTML Entities Converter', diff --git a/app/page.tsx b/app/page.tsx index 2413337..09e17c6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -304,7 +304,7 @@ const Page = () => {
{/* Sidebar */}