react-statix
Version:
React components for statix localization management
297 lines (282 loc) • 12.8 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import React, { createContext, useContext, useState, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
// src/context/StatixContext.tsx
const StatixContext = createContext(undefined);
const useStatix = () => {
const context = useContext(StatixContext);
if (!context) {
throw new Error("useStatix must be used within a StatixProvider");
}
return context;
};
var statix = "<svg\n width=\"40\"\n height=\"40\"\n viewBox=\"0 0 32 32\"\n fill=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n>\n <g>\n <g id=\"rotating-group\">\n <path\n d=\"M1.34391 26.0377L8.87169 18.9046C8.34648 17.8841 8.05 16.7267 8.05 15.5C8.05 14.6485 8.19286 13.8303 8.45593 13.0682L1.34098 6.434C1.02131 6.13593 0.5 6.36262 0.5 6.7997V25.6748C0.5 26.1138 1.02526 26.3397 1.34391 26.0377Z\"\n fill=\"black\"\n />\n <path\n d=\"M15.5 8.05C16.6959 8.05 17.8261 8.3318 18.8276 8.83261L26.0145 1.34627C26.3196 1.02846 26.0943 0.5 25.6538 0.5H6.11285C5.66963 0.5 5.44555 1.03396 5.75603 1.35026L12.82 8.5466C13.6516 8.22585 14.5553 8.05 15.5 8.05Z\"\n fill=\"black\"\n />\n <path\n d=\"M22.95 15.5C22.95 16.4683 22.7653 17.3934 22.4292 18.2421L30.6561 26.0377C30.9747 26.3397 31.5 25.6748 31.5 25.6748V6.36728C31.5 5.92519 30.9684 5.70061 30.6514 6.00882L22.7332 13.7086C22.8749 14.2824 22.95 14.8824 22.95 15.5Z\"\n fill=\"black\"\n />\n <path\n d=\"M15.5 22.95C16.4343 22.95 17.3284 22.778 18.1524 22.464L26.0145 30.6537C26.3196 30.9715 26.0943 31.5 25.6538 31.5H6.34622C5.90567 31.5 5.68043 30.9715 5.98552 30.6537L13.6126 22.7088C14.2154 22.8662 14.8479 22.95 15.5 22.95Z\"\n fill=\"black\"\n />\n </g>\n\n <path\n d=\"M18 9.5L12 16.5833H15.4286L14.2857 22L20 14.4167H16.5714L18 9.5Z\"\n fill=\"black\"\n >\n <animate\n attributeName=\"opacity\"\n values=\"0.5;1;0.5\"\n dur=\"1.5s\"\n repeatCount=\"indefinite\"\n />\n </path>\n </g>\n</svg>";
const StatixButton = ({ onClick }) => {
return (jsx("button", { onClick: onClick, style: {
position: "fixed",
backgroundColor: "white",
bottom: "12px",
right: "12px",
width: "50px",
height: "50px",
borderRadius: "8px",
border: "1px solid #e0e0e0",
boxShadow: "0 2px 10px rgba(0,0,0,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
cursor: "pointer",
zIndex: 10000000,
padding: "5px",
}, children: jsx("img", { src: statix, alt: "Statix" }) }));
};
const StatixContent = ({ isOpen, children, }) => {
return (jsx("div", { style: {
position: "fixed",
bottom: isOpen ? "0" : "-90vh",
left: "0",
width: "100%",
height: "90vh",
backgroundColor: "#ffffff",
boxShadow: "0 -2px 10px rgba(0,0,0,0.1)",
transition: "bottom 0.3s ease-in-out",
zIndex: 100000,
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
padding: "20px",
overflow: "auto",
}, children: jsx("div", { children: children }) }));
};
const StatixDrawer = ({ children }) => {
const { editable } = useStatix();
const [isOpen, setIsOpen] = useState(false);
const toggleDrawer = () => {
setIsOpen(!isOpen);
};
if (!editable)
return null;
return (jsxs(Fragment, { children: [jsx(StatixButton, { onClick: toggleDrawer }), jsx(StatixContent, { isOpen: isOpen, children: children })] }));
};
const fetchJSON = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}, url: ${url}`);
}
return await response.json();
};
const loadLocaleFiles = async (config) => {
const locales = {};
const langs = Object.keys(config.languagesKeys || {});
for (const lang of langs) {
const filename = `${config.localePath}/${lang}/translation.json`;
try {
locales[lang] = await fetchJSON(filename);
}
catch (e) {
console.warn(`Dil dosyası yüklenemedi: ${filename}`, e);
}
}
return locales;
};
const setNestedValue = (obj, path, value) => {
if (!obj || !path)
return obj;
const keys = path.split(".");
const lastKey = keys.pop();
let current = obj;
// Create or traverse the nested structure
for (const key of keys) {
if (current[key] === undefined ||
current[key] === null ||
typeof current[key] !== "object") {
current[key] = {};
}
current = current[key];
}
// Set the value at the final level
current[lastKey] = value;
return obj;
};
const defaultConfig = {
localePath: "public/locales",
languagesKeys: {},
};
const StatixProvider = ({ config = defaultConfig, children, }) => {
const [editable, setEditable] = useState(true);
const [locales, setLocales] = useState({});
const [pendingChanges, setPendingChanges] = useState(() => {
const saved = localStorage.getItem("localeEdits");
if (saved) {
try {
return JSON.parse(saved);
}
catch (error) {
console.warn("Invalid localStorage data for localeEdits, using empty object");
return {};
}
}
return {};
});
// Dil dosyalarını yükle
useEffect(() => {
const init = async () => {
const loadedLocales = await loadLocaleFiles(config);
setLocales(loadedLocales);
};
init();
}, []);
// LocalStorage'dan bekleyen değişiklikleri yükle
useEffect(() => {
if (Object.keys(pendingChanges).length === 0 &&
localStorage.getItem("localeEdits")) {
const saved = localStorage.getItem("localeEdits");
if (saved) {
try {
setPendingChanges(JSON.parse(saved));
}
catch (error) {
console.warn("Invalid localStorage data for localeEdits, skipping");
}
}
}
}, []);
// LocalStorage'a yaz
useEffect(() => {
if (Object.keys(pendingChanges).length > 0) {
localStorage.setItem("localeEdits", JSON.stringify(pendingChanges));
}
}, [pendingChanges]);
const updateLocalValue = (lang, key, value) => {
setPendingChanges((prev) => {
const updated = { ...prev };
updated[lang] = updated[lang] || {};
// Use setNestedValue to handle nested paths
setNestedValue(updated[lang], key, value);
return updated;
});
};
const resetChanges = () => {
setPendingChanges({});
localStorage.removeItem("localeEdits");
};
const saveChanges = () => {
if (config.onSave) {
// Use the custom save handler if provided
config.onSave(pendingChanges);
}
else {
// Default behavior
alert("Değişiklikler hazır!");
console.log("Payload:", pendingChanges);
}
// Optionally clear changes after saving
if (window.confirm("Değişiklikler kaydedildi. Yerel önbelleği temizlemek ister misiniz?")) {
resetChanges();
}
};
const contextValue = useMemo(() => ({
editable,
setEditable,
locales,
updateLocalValue,
pendingChanges,
resetChanges,
saveChanges,
}), [editable, locales, pendingChanges]);
return (jsxs(StatixContext.Provider, { value: contextValue, children: [children, jsx(StatixDrawer, {})] }));
};
const getNestedValue = (obj, path, defaultValue = undefined) => {
if (!obj || !path)
return defaultValue;
const keys = path.split(".");
let current = obj;
for (const key of keys) {
if (current === null ||
current === undefined ||
typeof current !== "object") {
return defaultValue;
}
current = current[key];
}
return current !== undefined ? current : defaultValue;
};
const Statix = ({ children, keyPath, lang }) => {
const { editable, locales, pendingChanges, updateLocalValue } = useStatix();
const triggerRef = useRef(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
const [show, setShow] = useState(false);
if (!editable)
return jsx(Fragment, { children: children });
const detectedKey = keyPath || children;
const currentLang = lang || Object.keys(locales)[0] || 'en';
const handleMouseEnter = () => {
if (triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
});
}
setShow(true);
};
const handleChange = (lang, e) => {
updateLocalValue(lang, detectedKey, e.target.value);
};
const translatedValue = getNestedValue(pendingChanges?.[currentLang], detectedKey) ??
getNestedValue(locales[currentLang], detectedKey);
return (jsxs("span", { ref: triggerRef, onMouseEnter: handleMouseEnter, onMouseLeave: () => setShow(false), children: [translatedValue || children, show && (jsx("div", { style: {
position: "absolute",
top: `${position.top}px`,
left: `${position.left}px`,
zIndex: 9999,
}, onMouseLeave: () => setShow(false), children: jsxs("div", { style: {
padding: "8px",
margin: "8px",
minWidth: "200px",
fontSize: "14px",
background: "#fff",
border: "1px solid #ccc",
borderRadius: "4px",
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
}, children: [jsx("strong", { style: { display: "block", marginBottom: "8px" }, children: detectedKey }), Object.keys(locales).map((lang) => (jsxs("div", { style: { marginBottom: "6px" }, children: [jsx("label", { style: {
fontWeight: "bold",
display: "block",
fontSize: "12px",
}, children: lang.toUpperCase() }), jsx("input", { type: "text", defaultValue: getNestedValue(pendingChanges?.[lang], detectedKey) ??
getNestedValue(locales[lang], detectedKey), onChange: (e) => handleChange(lang, e), style: {
width: "100%",
padding: "4px",
border: "1px solid #ccc",
borderRadius: "4px",
fontSize: "13px",
} })] }, lang)))] }) }))] }));
};
const useEditableTranslation = () => {
const { t, i18n } = useTranslation();
const { editable } = useStatix();
const wrappedT = (key, options) => {
const translatedValue = t(key, options);
// Ensure translatedValue is a valid ReactNode
const safeValue = typeof translatedValue === "string" ||
typeof translatedValue === "number" ||
typeof translatedValue === "boolean" ||
translatedValue === null ||
translatedValue === undefined ||
React.isValidElement(translatedValue)
? translatedValue
: String(translatedValue);
if (!editable) {
return safeValue;
}
// Ensure translatedValue is a string for Statix component
const stringValue = typeof safeValue === "string" ? safeValue : String(safeValue);
// Edit modunda Statix component’ini döndür
return jsx(Statix, { keyPath: key, children: stringValue });
};
return {
t: wrappedT,
i18n,
};
};
export { StatixProvider, useEditableTranslation, useStatix };
//# sourceMappingURL=index.esm.js.map