UNPKG

react-statix

Version:

React components for statix localization management

301 lines (285 loc) 13.1 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var React = require('react'); var reactI18next = require('react-i18next'); // src/context/StatixContext.tsx const StatixContext = React.createContext(undefined); const useStatix = () => { const context = React.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 (jsxRuntime.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: jsxRuntime.jsx("img", { src: statix, alt: "Statix" }) })); }; const StatixContent = ({ isOpen, children, }) => { return (jsxRuntime.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: jsxRuntime.jsx("div", { children: children }) })); }; const StatixDrawer = ({ children }) => { const { editable } = useStatix(); const [isOpen, setIsOpen] = React.useState(false); const toggleDrawer = () => { setIsOpen(!isOpen); }; if (!editable) return null; return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(StatixButton, { onClick: toggleDrawer }), jsxRuntime.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] = React.useState(true); const [locales, setLocales] = React.useState({}); const [pendingChanges, setPendingChanges] = React.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 React.useEffect(() => { const init = async () => { const loadedLocales = await loadLocaleFiles(config); setLocales(loadedLocales); }; init(); }, []); // LocalStorage'dan bekleyen değişiklikleri yükle React.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 React.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 = React.useMemo(() => ({ editable, setEditable, locales, updateLocalValue, pendingChanges, resetChanges, saveChanges, }), [editable, locales, pendingChanges]); return (jsxRuntime.jsxs(StatixContext.Provider, { value: contextValue, children: [children, jsxRuntime.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 = React.useRef(null); const [position, setPosition] = React.useState({ top: 0, left: 0 }); const [show, setShow] = React.useState(false); if (!editable) return jsxRuntime.jsx(jsxRuntime.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 (jsxRuntime.jsxs("span", { ref: triggerRef, onMouseEnter: handleMouseEnter, onMouseLeave: () => setShow(false), children: [translatedValue || children, show && (jsxRuntime.jsx("div", { style: { position: "absolute", top: `${position.top}px`, left: `${position.left}px`, zIndex: 9999, }, onMouseLeave: () => setShow(false), children: jsxRuntime.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: [jsxRuntime.jsx("strong", { style: { display: "block", marginBottom: "8px" }, children: detectedKey }), Object.keys(locales).map((lang) => (jsxRuntime.jsxs("div", { style: { marginBottom: "6px" }, children: [jsxRuntime.jsx("label", { style: { fontWeight: "bold", display: "block", fontSize: "12px", }, children: lang.toUpperCase() }), jsxRuntime.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 } = reactI18next.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 jsxRuntime.jsx(Statix, { keyPath: key, children: stringValue }); }; return { t: wrappedT, i18n, }; }; exports.StatixProvider = StatixProvider; exports.useEditableTranslation = useEditableTranslation; exports.useStatix = useStatix; //# sourceMappingURL=index.js.map