remove unused utils, convert utils to TS

This commit is contained in:
Alex Holliday
2026-02-12 19:13:53 +00:00
parent 4e3a82d25f
commit cafea2bf5d
19 changed files with 40 additions and 711 deletions
-8
View File
@@ -6,7 +6,6 @@ import { ThemeProvider } from "@emotion/react";
import lightTheme from "./Utils/Theme/lightTheme";
import darkTheme from "./Utils/Theme/darkTheme";
import { CssBaseline, GlobalStyles } from "@mui/material";
import { logger } from "./Utils/Logger"; // Import the logger
import { Routes } from "./Routes";
import AppLayout from "@/Components/v2/layout/AppLayout";
import type { RootState } from "@/Types/state";
@@ -14,13 +13,6 @@ import type { RootState } from "@/Types/state";
function App() {
const mode = useSelector((state: RootState) => state.ui.mode);
// Cleanup
useEffect(() => {
return () => {
logger.cleanup();
};
}, []);
const theme = mode === "light" ? lightTheme : darkTheme;
return (
@@ -1,8 +1,9 @@
import i18n from "../../../Utils/i18n.js";
import i18n from "@/Utils/i18n.js";
import { useSelector } from "react-redux";
import { useEffect } from "react";
import type { RootState } from "@/store";
const I18nLoader = () => {
const language = useSelector((state) => state.ui.language ?? "en");
const language = useSelector((state: RootState) => state.ui.language ?? "en");
useEffect(() => {
if (language && i18n.language !== language) {
@@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography";
import { BaseChart } from "@/Components/v2/design-elements";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import { formatDateWithTz } from "@/Utils/timeUtilsLegacy";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import { useTranslation } from "react-i18next";
import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts";
import { getResponseTimeColor } from "@/Utils/MonitorUtils";
+1 -15
View File
@@ -12,11 +12,6 @@ interface SidebarState {
collapsed: boolean;
}
interface GreetingState {
index: number;
lastUpdate: string | null;
}
interface UIState {
monitors: TableState;
team: TableState;
@@ -26,7 +21,6 @@ interface UIState {
sidebar: SidebarState;
mode: ThemeMode;
showURL: boolean;
greeting: GreetingState;
timezone: string;
distributedUptimeEnabled: boolean;
language: string;
@@ -60,7 +54,6 @@ const initialState: UIState = {
},
mode: initialMode,
showURL: false,
greeting: { index: 0, lastUpdate: null },
timezone: "America/Toronto",
distributedUptimeEnabled: false,
language: "en",
@@ -94,13 +87,7 @@ const uiSlice = createSlice({
setShowURL: (state, action: PayloadAction<boolean>) => {
state.showURL = action.payload;
},
setGreeting: (
state,
action: PayloadAction<{ index: number; lastUpdate: string | null }>
) => {
state.greeting.index = action.payload.index;
state.greeting.lastUpdate = action.payload.lastUpdate;
},
setTimezone: (state, action: PayloadAction<{ timezone: string }>) => {
state.timezone = action.payload.timezone;
},
@@ -124,7 +111,6 @@ export const {
setCollapsed,
setMode,
setShowURL,
setGreeting,
setTimezone,
setDistributedUptimeEnabled,
setLanguage,
@@ -9,13 +9,13 @@ import type { Monitor } from "@/Types/Monitor";
import type { ActionMenuItem } from "@/Components/v2/actions-menu";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { TypeToPathMap } from "@/Utils/monitorUtilsLegacy.js";
import { formatDateWithTz } from "@/Utils/TimeUtils";
import { useSelector } from "react-redux";
import type { RootState } from "@/Types/state";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import { getMonitorPath } from "@/Utils/MonitorUtils";
interface IncidentsTableProps {
title?: string;
@@ -62,7 +62,7 @@ export const IncidentsTable = ({
label: t("pages.incidents.table.actions.goToMonitor"),
action: () => {
if (monitor) {
const path = TypeToPathMap[monitor.type as keyof typeof TypeToPathMap];
const path = getMonitorPath(monitor.type);
if (path && monitor.id) {
navigate(`/${path}/${monitor.id}`);
}
-74
View File
@@ -1,74 +0,0 @@
const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug";
const NO_OP = () => {};
class Logger {
constructor() {
let logLevel = LOG_LEVEL;
this.updateLogLevel(logLevel);
// Defer store subscription to avoid circular dependency during HMR
setTimeout(() => {
try {
import("../store").then(({ default: store }) => {
if (store) {
this.unsubscribe = store.subscribe(() => {
logLevel = "debug";
this.updateLogLevel(logLevel);
});
}
});
} catch (e) {
// Store not ready yet, ignore
}
}, 0);
}
updateLogLevel(logLevel) {
if (logLevel === "none") {
this.info = NO_OP;
this.error = NO_OP;
this.warn = NO_OP;
this.log = NO_OP;
return;
}
if (logLevel === "error") {
this.error = console.error.bind(console);
this.info = NO_OP;
this.warn = NO_OP;
this.log = NO_OP;
return;
}
if (logLevel === "warn") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = NO_OP;
this.log = NO_OP;
return;
}
if (logLevel === "info") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = console.info.bind(console);
this.log = NO_OP;
return;
}
if (logLevel === "debug") {
this.error = console.error.bind(console);
this.warn = console.warn.bind(console);
this.info = console.info.bind(console);
this.log = console.log.bind(console);
return;
}
}
cleanup() {
if (this.unsubscribe) {
this.unsubscribe();
}
}
}
export const logger = new Logger();
+4 -4
View File
@@ -7,11 +7,11 @@ export const getMonitorPath = (type: MonitorType): string => {
http: "uptime",
port: "uptime",
ping: "uptime",
hardware: "hardware",
pagespeed: "pagespeed",
docker: "docker",
game: "game-servers",
game: "uptime",
unknown: "uptime",
docker: "uptime",
hardware: "infrastructure",
pagespeed: "pagespeed",
};
return pathMap[type];
};
-1
View File
@@ -1 +0,0 @@
#Utils folder
-11
View File
@@ -1,11 +0,0 @@
export const formatBytes = (bytes) => {
if (bytes === 0) return "0 Bytes";
const megabytes = bytes / (1024 * 1024);
return megabytes.toFixed(2) + " MB";
};
export const checkImage = (url) => {
const img = new Image();
img.src = url;
return img.naturalWidth !== 0;
};
-197
View File
@@ -1,197 +0,0 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Typography } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { setGreeting } from "../Features/UI/uiSlice";
const early = [
{
prepend: "Rise and shine",
append: "If youre up this early, you might as well be a legend!",
emoji: "☕",
},
{
prepend: "Good morning",
append: "The worlds still asleep, but youre already awesome!",
emoji: "🦉",
},
{
prepend: "Good morning",
append: "Are you a wizard? Only magical people are up at this hour!",
emoji: "🌄",
},
{
prepend: "Up before the roosters",
append: "Ready to tackle the day before it even starts?",
emoji: "🐓",
},
{
prepend: "Early bird special",
append: "Lets get things done while everyone else is snoozing!",
emoji: "🌟",
},
];
const morning = [
{
prepend: "Good morning",
append: "Is it coffee oclock yet, or should we start with high fives?",
emoji: "☕",
},
{
prepend: "Morning",
append: "The sun is up, and so are you—time to be amazing!",
emoji: "🌞",
},
{
prepend: "Good morning",
append: "Time to make today the best thing since sliced bread!",
emoji: "🥐",
},
{
prepend: "Morning",
append: "Lets kick off the day with more energy than a double espresso!",
emoji: "🚀",
},
{
prepend: "Rise and shine",
append: "Youre about to make today so great, even Monday will be jealous!",
emoji: "🌟",
},
];
const afternoon = [
{
prepend: "Good afternoon",
append: "How about a break to celebrate how awesome youre doing?",
emoji: "🥪",
},
{
prepend: "Afternoon",
append: "If youre still going strong, youre officially a rockstar!",
emoji: "🌞",
},
{
prepend: "Hey there",
append: "The afternoon is your playground—lets make it epic!",
emoji: "🍕",
},
{
prepend: "Good afternoon",
append: "Time to crush the rest of the day like a pro!",
emoji: "🏆",
},
{
prepend: "Afternoon",
append: "Time to turn those afternoon slumps into afternoon triumphs!",
emoji: "🎉",
},
];
const evening = [
{
prepend: "Good evening",
append: "Time to wind down and think about how you crushed today!",
emoji: "🌇",
},
{
prepend: "Evening",
append: "Youve earned a break—lets make the most of these evening vibes!",
emoji: "🍹",
},
{
prepend: "Hey there",
append: "Time to relax and bask in the glow of your days awesomeness!",
emoji: "🌙",
},
{
prepend: "Good evening",
append: "Ready to trade productivity for chill mode?",
emoji: "🛋️ ",
},
{
prepend: "Evening",
append: "Lets call it a day and toast to your success!",
emoji: "🕶️",
},
];
/**
* Greeting component that displays a personalized greeting message
* based on the time of day and the user's first name.
*
* @component
* @example
* return <Greeting type={"pagespeed"} />;
*
* @param {Object} props
* @param {string} props.type - The type of monitor to be displayed in the message
* @returns {JSX.Element} The rendered Greeting component
*/
const Greeting = ({ type = "" }) => {
const theme = useTheme();
const dispatch = useDispatch();
const { t } = useTranslation();
const { firstName } = useSelector((state) => state.auth.user);
const index = useSelector((state) => state.ui.greeting?.index ?? 0);
const lastUpdate = useSelector((state) => state.ui.greeting?.lastUpdate ?? null);
const now = new Date();
const hour = now.getHours();
useEffect(() => {
const hourDiff = lastUpdate ? hour - lastUpdate : null;
if (!lastUpdate || hourDiff >= 1) {
let random = Math.floor(Math.random() * 5);
dispatch(setGreeting({ index: random, lastUpdate: hour }));
}
}, [dispatch, hour, lastUpdate]);
let greetingArray =
hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening;
const { prepend, append, emoji } = greetingArray[index];
return (
<Box>
<Typography
component="h1"
variant="h1"
mb={theme.spacing(1)}
>
<Typography
component="span"
fontSize="inherit"
color={theme.palette.primary.contrastTextTertiary}
>
{t("greeting.prepend", { defaultValue: prepend })},{" "}
</Typography>
<Typography
component="span"
fontSize="inherit"
fontWeight="inherit"
color={theme.palette.primary.contrastTextSecondary}
>
{firstName} {emoji}
</Typography>
</Typography>
<Typography
variant="h2"
lineHeight={1}
color={theme.palette.primary.contrastTextTertiary}
>
{t("greeting.append", { defaultValue: append })} {" "}
{t("greeting.overview", { type: t(`menu.${type}`) })}
</Typography>
</Box>
);
};
Greeting.propTypes = {
type: PropTypes.string,
};
export default Greeting;
@@ -1,16 +1,26 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import type { Resource } from "i18next";
const primaryLanguage = "en";
// Load all translation files eagerly
const translations = import.meta.glob("../locales/*.json", { eager: true });
interface TranslationModule {
default?: Record<string, unknown>;
[key: string]: unknown;
}
const resources = {};
// Load all translation files eagerly
const translations = import.meta.glob<TranslationModule>("../locales/*.json", {
eager: true,
});
const resources: Resource = {};
Object.keys(translations).forEach((path) => {
const langCode = path.match(/\/([^/]+)\.json$/)[1];
const match = path.match(/\/([^/]+)\.json$/);
if (!match) return;
const langCode = match[1];
resources[langCode] = {
translation: translations[path].default || translations[path],
translation: translations[path].default ?? translations[path],
};
});
-47
View File
@@ -1,47 +0,0 @@
import { capitalizeFirstLetter } from "./stringUtils";
/**
* Helper function to get duration since last check or the last date checked
* @param {Array} checks Array of check objects.
* @param {boolean} duration Whether the function should return the duration since last checked or the date itself
* @returns {number} Timestamp of the most recent check.
*/
export const getLastChecked = (checks, duration = true) => {
if (!checks || checks.length === 0) {
return 0; // Handle case when no checks are available
}
// Data is sorted newest -> oldest, so newest check is the most recent
if (!duration) {
return new Date(checks[0].createdAt);
}
return new Date() - new Date(checks[0].createdAt);
};
export const parseDomainName = (url) => {
url = url.replace(/^https?:\/\//, "");
// Remove leading/trailing dots
url = url.replace(/^\.+|\.+$/g, "");
// Split by dots
const parts = url.split(".");
// Remove common prefixes and empty parts and exclude the last element of the array (the last element should be the TLD)
const cleanParts = parts.filter((part) => part !== "www" && part !== "").slice(0, -1);
// If there's more than one part, append the two words and capitalize the first letters (e.g. ["api", "test"] -> "Api Test")
const domainPart =
cleanParts.length > 1
? cleanParts.map((part) => capitalizeFirstLetter(part)).join(" ")
: capitalizeFirstLetter(cleanParts[0]);
if (domainPart) return domainPart;
return url;
};
export const TypeToPathMap = {
http: "uptime",
port: "uptime",
docker: "uptime",
ping: "uptime",
hardware: "infrastructure",
pagespeed: "pagespeed",
};
-13
View File
@@ -1,13 +0,0 @@
export const ROLES = {
SUPERADMIN: "superadmin",
ADMIN: "admin",
USER: "user",
DEMO: "demo",
};
export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO];
export const EDITABLE_ROLES = [
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
{ role: ROLES.USER, _id: ROLES.USER },
];
-46
View File
@@ -1,46 +0,0 @@
/**
* Helper function to get first letter capitalized string
* @param {string} str String whose first letter is to be capitalized
* @returns A string with first letter capitalized
*/
export const capitalizeFirstLetter = (str) => {
if (str === null || str === undefined) {
return "";
}
if (typeof str !== "string") {
throw new TypeError("Input must be a string");
}
if (str.length === 0) {
return "";
}
return str.charAt(0).toUpperCase() + str.slice(1);
};
/**
* Helper function to get first letter as a lower case string
* @param {string} str String whose first letter is to be lower cased
* @returns A string with first letter lower cased
*/
export const toLowerCaseFirstLetter = (str) => {
if (str === null || str === undefined) {
return "";
}
if (typeof str !== "string") {
throw new TypeError("Input must be a string");
}
if (str.length === 0) {
return "";
}
return str.charAt(0).toLowerCase() + str.slice(1);
};
/**
* Checks if a string is null, undefined, or empty (including strings with only whitespace).
* @param {string} str - The string to check.
* @returns {boolean} - Returns true if the string is null, undefined, or empty.
*/
export const isEmpty = (str) => {
// Check if string is null, undefined, or empty (including whitespace only)
return str === null || typeof str === "undefined" || str.trim().length === 0;
};
-167
View File
@@ -1,167 +0,0 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(utc);
dayjs.extend(timezone);
dayjs.extend(customParseFormat);
dayjs.extend(duration);
export const MS_PER_SECOND = 1000;
export const MS_PER_MINUTE = 60 * MS_PER_SECOND;
export const MS_PER_HOUR = 60 * MS_PER_MINUTE;
export const MS_PER_DAY = 24 * MS_PER_HOUR;
export const MS_PER_WEEK = MS_PER_DAY * 7;
export const formatDuration = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let dateStr = "";
days && (dateStr += `${days}d `);
hours && (dateStr += `${hours % 24}h `);
minutes && (dateStr += `${minutes % 60}m `);
seconds && (dateStr += `${seconds % 60}s `);
dateStr === "" && (dateStr = "0s");
return dateStr;
};
export const formatDurationRounded = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
let time = "";
if (days > 0) {
time += `${days} day${days !== 1 ? "s" : ""}`;
return time;
}
if (hours > 0) {
time += `${hours} hour${hours !== 1 ? "s" : ""}`;
return time;
}
if (minutes > 0) {
time += `${minutes} minute${minutes !== 1 ? "s" : ""}`;
return time;
}
if (seconds > 0) {
time += `${seconds} second${seconds !== 1 ? "s" : ""}`;
return time;
}
return time;
};
export const formatDurationSplit = (ms) => {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
return days > 0
? { time: days, format: days === 1 ? "day" : "days" }
: hours > 0
? { time: hours, format: hours === 1 ? "hour" : "hours" }
: minutes > 0
? { time: minutes, format: minutes === 1 ? "minute" : "minutes" }
: seconds > 0
? { time: seconds, format: seconds === 1 ? "second" : "seconds" }
: { time: 0, format: "seconds" };
};
export const getHumanReadableDuration = (ms) => {
const durationObj = dayjs.duration(ms);
const parts = {
days: Math.floor(durationObj.asDays()),
hours: durationObj.hours(),
minutes: durationObj.minutes(),
seconds: durationObj.seconds(),
milliseconds: durationObj.milliseconds(),
};
const result = [];
if (parts.days > 0) {
result.push(`${parts.days}d`);
}
if (parts.hours > 0) {
result.push(`${parts.hours}h`);
}
if (result.length < 2 && parts.minutes > 0) {
result.push(`${parts.minutes}m`);
}
if (result.length < 2 && parts.seconds > 0) {
result.push(`${parts.seconds}s`);
}
if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) {
result.push(`${parts.milliseconds.toFixed(2)}ms`);
}
if (result.length === 0) {
// fallback for durations < 1s
return "0s";
}
return result.join(" ");
};
export const formatDate = (date, customOptions) => {
const options = {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
...customOptions,
};
// Return the date using the specified options
return date
.toLocaleString("en-US", options)
.replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase());
};
export const formatDateWithTz = (timestamp, format, timezone) => {
const formattedDate = dayjs(timestamp).tz(timezone).format(format);
return formattedDate;
};
export const tickDateFormatLookup = (range) => {
switch (range) {
case "recent":
return "h:mm A";
case "day":
return "h A";
case "week":
return "MMM D";
case "month":
return "MMM D";
default:
return "MMM D, h A";
}
};
export const tooltipDateFormatLookup = (range) => {
switch (range) {
case "recent":
return "MMM D, h:mm A";
case "day":
return "MMM D, h:mm A";
case "week":
return "ddd, MMM D";
case "month":
return "ddd, MMM D";
default:
return "MMM D, h:mm A";
}
};
-15
View File
@@ -1,15 +0,0 @@
export const safelyParseFloat = (value) => {
const parsedValue = parseFloat(value);
if (isNaN(parsedValue)) {
return 0;
}
return parsedValue;
};
export const formatMonitorUrl = (url, maxLength = 55) => {
if (!url) return "";
const strippedUrl = url.replace(/^https?:\/\//, "");
return strippedUrl.length > maxLength
? `${strippedUrl.slice(0, maxLength)}`
: strippedUrl;
};
-101
View File
@@ -1,101 +0,0 @@
/**
* Update errors if passed id matches the error.details[0].path, otherwise remove
* the error for the id
* @param {*} prev Previous errors *
* @param {*} id ID of the field whose error is to be either updated or removed
* @param {*} error the error object
* @returns the Update Errors with the specific field with id being either removed or updated
*/
const buildErrors = (prev, id, error) => {
const updatedErrors = { ...prev };
if (error && id == error.details[0].path) {
updatedErrors[id] = error.details[0].message ?? "Validation error";
} else {
delete updatedErrors[id];
}
return updatedErrors;
};
/**
* Processes Joi validation errors and returns a filtered object of error messages for fields that have been touched.
*
* @param {Object} validation - The Joi validation result object.
* @param {Object} validation.error - The error property of the validation result containing details of validation failures.
* @param {Object[]} validation.error.details - An array of error details from the Joi validation. Each item contains information about the path and the message.
* @param {Object} touchedErrors - An object representing which fields have been interacted with. Keys are field IDs (field names), and values are booleans indicating whether the field has been touched.
* @returns {Object} - An object where keys are the field IDs (if they exist in `touchedErrors` and are in the error details) and values are their corresponding error messages.
*/
const getTouchedFieldErrors = (validation, touchedErrors) => {
let newErrors = {};
if (validation?.error) {
newErrors = validation.error.details.reduce((errors, detail) => {
const fieldId = detail.path[0];
if (touchedErrors[fieldId] && !(fieldId in errors)) {
errors[fieldId] = detail.message;
}
return errors;
}, {});
}
return newErrors;
};
/**
*
* @param {*} form The form object of the submitted form data
* @param {*} validation The Joi validation rules
* @param {*} setErrors The function used to set the local errors
* @returns true if there is no error or false if there is error after validating the form
* the error will be reset to {} if returns false; otherwise the errors object will be set with
* the new value
*/
const hasValidationErrors = (form, validation, setErrors) => {
const { error } = validation.validate(form, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
if (
![
"clientHost",
"refreshTokenSecret",
"dbConnectionString",
"refreshTokenTTL",
"jwtTTL",
"notify-email-list",
"_id",
"__v",
"createdAt",
"updatedAt",
].includes(err.path[0])
) {
newErrors[err.path[0]] = err.message ?? "Validation error";
}
// Handle conditionally usage number required cases
if (!form.cpu || form.usage_cpu) {
newErrors["usage_cpu"] = null;
}
if (!form.memory || form.usage_memory) {
newErrors["usage_memory"] = null;
}
if (!form.disk || form.usage_disk) {
newErrors["usage_disk"] = null;
}
if (!form.temperature || form.usage_temperature) {
newErrors["usage_temperature"] = null;
}
});
if (Object.values(newErrors).some((v) => v)) {
setErrors(newErrors);
return true;
} else {
setErrors({});
return false;
}
}
setErrors({});
return false;
};
export { buildErrors, hasValidationErrors, getTouchedFieldErrors };
+13 -1
View File
@@ -1,6 +1,18 @@
import joi from "joi";
import dayjs from "dayjs";
import { ROLES } from "../Utils/roleUtils";
export const ROLES = {
SUPERADMIN: "superadmin",
ADMIN: "admin",
USER: "user",
DEMO: "demo",
};
export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO];
export const EDITABLE_ROLES = [
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
{ role: ROLES.USER, _id: ROLES.USER },
];
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
+1 -1
View File
@@ -5,7 +5,7 @@ import { BrowserRouter as Router } from "react-router-dom";
import { Provider } from "react-redux";
import { persistor, store } from "@/store.js";
import { PersistGate } from "redux-persist/integration/react";
import I18nLoader from "./Components/v1/I18nLoader/index.jsx";
import I18nLoader from "./Components/v2/i18nLoader";
import { initApiClient } from "./Utils/ApiClient.js";
initApiClient(store);