mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-05-05 14:47:12 -04:00
Added react i18next.
This commit is contained in:
parent
6f440a13cd
commit
61a95c1630
13 changed files with 449 additions and 32 deletions
|
@ -11,8 +11,12 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"i18next": "^23.10.1",
|
||||||
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^14.1.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
|
@ -28,6 +32,7 @@
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.1.4",
|
"vite": "^5.1.4",
|
||||||
|
"vite-plugin-static-copy": "^1.0.1",
|
||||||
"vite-plugin-svgr": "^4.2.0"
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,17 @@ import {isJSONClean} from './utils/utils.ts'
|
||||||
import {NavLink, Outlet} from "react-router-dom";
|
import {NavLink, Outlet} from "react-router-dom";
|
||||||
import {useStore} from "./store/store.ts";
|
import {useStore} from "./store/store.ts";
|
||||||
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
||||||
|
import {ToastDialog} from "./utils/Toast.tsx";
|
||||||
|
import {useTranslation} from "react-i18next";
|
||||||
|
|
||||||
|
|
||||||
export const App = ()=> {
|
export const App = ()=> {
|
||||||
const setSettings = useStore(state => state.setSettings);
|
const setSettings = useStore(state => state.setSettings);
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
document.title = t('admin.page-title')
|
||||||
|
|
||||||
useStore.getState().setShowLoading(true);
|
useStore.getState().setShowLoading(true);
|
||||||
const settingSocket = connect('http://localhost:9001/settings', {
|
const settingSocket = connect('http://localhost:9001/settings', {
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
|
@ -26,13 +32,18 @@ export const App = ()=> {
|
||||||
|
|
||||||
settingSocket.on('connect', () => {
|
settingSocket.on('connect', () => {
|
||||||
useStore.getState().setSettingsSocket(settingSocket);
|
useStore.getState().setSettingsSocket(settingSocket);
|
||||||
|
useStore.getState().setShowLoading(false)
|
||||||
settingSocket.emit('load');
|
settingSocket.emit('load');
|
||||||
console.log('connected');
|
console.log('connected');
|
||||||
});
|
});
|
||||||
|
|
||||||
settingSocket.on('disconnect', (reason) => {
|
settingSocket.on('disconnect', (reason) => {
|
||||||
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
// The settingSocket.io client will automatically try to reconnect for all reasons other than "io
|
||||||
// server disconnect".
|
// server disconnect".
|
||||||
if (reason === 'io server disconnect') settingSocket.connect();
|
useStore.getState().setShowLoading(true)
|
||||||
|
if (reason === 'io server disconnect') {
|
||||||
|
settingSocket.connect();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
settingSocket.on('settings', (settings) => {
|
settingSocket.on('settings', (settings) => {
|
||||||
|
@ -63,6 +74,7 @@ export const App = ()=> {
|
||||||
|
|
||||||
return <div id="wrapper">
|
return <div id="wrapper">
|
||||||
<LoadingScreen/>
|
<LoadingScreen/>
|
||||||
|
<ToastDialog/>
|
||||||
<div className="menu">
|
<div className="menu">
|
||||||
<h1>Etherpad</h1>
|
<h1>Etherpad</h1>
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
@ -14,10 +14,6 @@ html, body, #root {
|
||||||
box-sizing: inherit;
|
box-sizing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
@ -64,7 +60,7 @@ div.innerwrapper-err {
|
||||||
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
|
box-shadow: 0px 1px 10px rgba(0, 0, 0, 0.2);
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 1150px;
|
max-width: 1150px;
|
||||||
min-height: 101%;/*always display a scrollbar*/
|
min-height: 100%;/*always display a scrollbar*/
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,3 +353,107 @@ pre {
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.ToastViewport {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 390px;
|
||||||
|
max-width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
z-index: 2147483647;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastRootSuccess {
|
||||||
|
background-color: lawngreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastRootFailure {
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastRootFailure > .ToastTitle {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastRoot {
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px;
|
||||||
|
padding: 15px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'title action' 'description action';
|
||||||
|
grid-template-columns: auto max-content;
|
||||||
|
column-gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state='open'] {
|
||||||
|
animation: slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.ToastRoot[data-state='closed'] {
|
||||||
|
animation: hide 100ms ease-in;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='move'] {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='cancel'] {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: transform 200ms ease-out;
|
||||||
|
}
|
||||||
|
.ToastRoot[data-swipe='end'] {
|
||||||
|
animation: swipeOut 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hide {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipeOut {
|
||||||
|
from {
|
||||||
|
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(calc(100% + var(--viewport-padding)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastTitle {
|
||||||
|
grid-area: title;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--slate-12);
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastDescription {
|
||||||
|
grid-area: description;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--slate-11);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ToastAction {
|
||||||
|
grid-area: action;
|
||||||
|
}
|
||||||
|
|
48
admin/src/localization/i18n.ts
Normal file
48
admin/src/localization/i18n.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import i18n from 'i18next'
|
||||||
|
import {initReactI18next} from "react-i18next";
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||||
|
|
||||||
|
|
||||||
|
import { BackendModule } from 'i18next';
|
||||||
|
|
||||||
|
const LazyImportPlugin: BackendModule = {
|
||||||
|
type: 'backend',
|
||||||
|
init: function (services, backendOptions, i18nextOptions) {
|
||||||
|
},
|
||||||
|
read: async function (language, namespace, callback) {
|
||||||
|
console.log(import.meta.env.BASE_URL+`/locales/${language}.json`)
|
||||||
|
const localeJSON = await fetch(import.meta.env.BASE_URL+`/locales/${language}.json`)
|
||||||
|
let json;
|
||||||
|
|
||||||
|
try {
|
||||||
|
json = JSON.parse(await localeJSON.text())
|
||||||
|
} catch(e) {
|
||||||
|
callback(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
callback(null, json);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function (language, namespace, data) {
|
||||||
|
},
|
||||||
|
|
||||||
|
create: function (languages, namespace, key, fallbackValue) {
|
||||||
|
/* save the missing translation */
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(LazyImportPlugin)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init(
|
||||||
|
{
|
||||||
|
backend:{
|
||||||
|
loadPath: import.meta.env.BASE_URL+'/locales/{{lng}}-{{ns}}.json'
|
||||||
|
},
|
||||||
|
fallbackLng: 'en'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default i18n
|
|
@ -7,6 +7,9 @@ import {HomePage} from "./pages/HomePage.tsx";
|
||||||
import {SettingsPage} from "./pages/SettingsPage.tsx";
|
import {SettingsPage} from "./pages/SettingsPage.tsx";
|
||||||
import {LoginScreen} from "./pages/LoginScreen.tsx";
|
import {LoginScreen} from "./pages/LoginScreen.tsx";
|
||||||
import {HelpPage} from "./pages/HelpPage.tsx";
|
import {HelpPage} from "./pages/HelpPage.tsx";
|
||||||
|
import * as Toast from '@radix-ui/react-toast'
|
||||||
|
import {I18nextProvider} from "react-i18next";
|
||||||
|
import i18n from "./localization/i18n.ts";
|
||||||
|
|
||||||
const router = createBrowserRouter(createRoutesFromElements(
|
const router = createBrowserRouter(createRoutesFromElements(
|
||||||
<><Route element={<App/>}>
|
<><Route element={<App/>}>
|
||||||
|
@ -17,12 +20,17 @@ const router = createBrowserRouter(createRoutesFromElements(
|
||||||
</Route><Route path="/login">
|
</Route><Route path="/login">
|
||||||
<Route index element={<LoginScreen/>}/>
|
<Route index element={<LoginScreen/>}/>
|
||||||
</Route></>
|
</Route></>
|
||||||
))
|
), {
|
||||||
|
basename: import.meta.env.BASE_URL
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<RouterProvider router={router}>
|
<I18nextProvider i18n={i18n}>
|
||||||
</RouterProvider>
|
<Toast.Provider>
|
||||||
|
<RouterProvider router={router}/>
|
||||||
|
</Toast.Provider>
|
||||||
|
</I18nextProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,48 +1,161 @@
|
||||||
import {useStore} from "../store/store.ts";
|
import {useStore} from "../store/store.ts";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {PluginDef} from "./Plugin.ts";
|
import {InstalledPlugin, PluginDef, SearchParams} from "./Plugin.ts";
|
||||||
|
import {useDebounce} from "../utils/useDebounce.ts";
|
||||||
|
|
||||||
|
|
||||||
export const HomePage = () => {
|
export const HomePage = () => {
|
||||||
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
const pluginsSocket = useStore(state=>state.pluginsSocket)
|
||||||
const [limit, setLimit] = useState(20)
|
|
||||||
const [offset, setOffset] = useState(0)
|
|
||||||
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
const [plugins,setPlugins] = useState<PluginDef[]>([])
|
||||||
|
const [installedPlugins, setInstalledPlugins] = useState<InstalledPlugin[]>([])
|
||||||
|
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||||
|
offset: 0,
|
||||||
|
limit: 99999,
|
||||||
|
sortBy: 'name',
|
||||||
|
sortDir: 'asc',
|
||||||
|
searchTerm: ''
|
||||||
|
})
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
pluginsSocket?.emit('search', {
|
if(!pluginsSocket){
|
||||||
searchTerm: '',
|
return
|
||||||
offset: offset,
|
}
|
||||||
limit: limit,
|
|
||||||
sortBy: 'name',
|
|
||||||
sortDir: 'asc'
|
|
||||||
})
|
|
||||||
setOffset(offset+limit)
|
|
||||||
|
|
||||||
pluginsSocket!.on('results:search', (data) => {
|
pluginsSocket.on('results:installed', (data:{
|
||||||
|
installed: InstalledPlugin[]
|
||||||
|
})=>{
|
||||||
|
setInstalledPlugins(data.installed)
|
||||||
|
})
|
||||||
|
|
||||||
|
pluginsSocket.on('results:updatable', () => {
|
||||||
|
console.log("Finished install")
|
||||||
|
})
|
||||||
|
|
||||||
|
pluginsSocket.on('finished:install', () => {
|
||||||
|
pluginsSocket!.emit('getInstalled');
|
||||||
|
})
|
||||||
|
|
||||||
|
pluginsSocket.on('finished:uninstall', () => {
|
||||||
|
console.log("Finished uninstall")
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Reload on reconnect
|
||||||
|
pluginsSocket.on('connect', ()=>{
|
||||||
|
// Initial retrieval of installed plugins
|
||||||
|
pluginsSocket.emit('getInstalled');
|
||||||
|
pluginsSocket.emit('search', searchParams)
|
||||||
|
})
|
||||||
|
|
||||||
|
pluginsSocket.emit('getInstalled');
|
||||||
|
|
||||||
|
// check for updates every 5mins
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
pluginsSocket.emit('checkUpdates');
|
||||||
|
}, 1000 * 60 * 5);
|
||||||
|
|
||||||
|
return ()=>{
|
||||||
|
clearInterval(interval)
|
||||||
|
}
|
||||||
|
}, [pluginsSocket]);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pluginsSocket) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginsSocket?.emit('search', searchParams)
|
||||||
|
|
||||||
|
|
||||||
|
pluginsSocket!.on('results:search', (data: {
|
||||||
|
results: PluginDef[]
|
||||||
|
}) => {
|
||||||
setPlugins(data.results)
|
setPlugins(data.results)
|
||||||
})
|
})
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
}, [searchParams, pluginsSocket]);
|
||||||
|
|
||||||
|
const uninstallPlugin = (pluginName: string)=>{
|
||||||
|
pluginsSocket!.emit('uninstall', pluginName);
|
||||||
|
// Remove plugin
|
||||||
|
setInstalledPlugins(installedPlugins.filter(i=>i.name !== pluginName))
|
||||||
|
}
|
||||||
|
|
||||||
|
const installPlugin = (pluginName: string)=>{
|
||||||
|
pluginsSocket!.emit('install', pluginName);
|
||||||
|
setPlugins(plugins.filter(plugin=>plugin.name !== pluginName))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useDebounce(()=>{
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
offset: 0,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
})
|
||||||
|
}, 500, [searchTerm])
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<h1>Home Page</h1>
|
<h1>Home Page</h1>
|
||||||
|
|
||||||
|
<h2>Installierte Plugins</h2>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Description</th>
|
<th>Version</th>
|
||||||
<th>Action</th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody style={{overflow: 'auto'}}>
|
||||||
{plugins.map((plugin, index) => {
|
{installedPlugins.map((plugin, index) => {
|
||||||
return <tr key={index}>
|
return <tr key={index}>
|
||||||
<td>{plugin.name}</td>
|
<td>{plugin.name}</td>
|
||||||
<td>{plugin.description}</td>
|
<td>{plugin.version}</td>
|
||||||
<td>test</td>
|
<td onClick={() => {
|
||||||
|
}}>
|
||||||
|
<button disabled={plugin.name == "ep_etherpad-lite"} onClick={() => uninstallPlugin(plugin.name)}>Entfernen</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
<h2>Verfügbare Plugins</h2>
|
||||||
|
|
||||||
|
<input type="text" value={searchTerm} onChange={v=>{
|
||||||
|
setSearchTerm(v.target.value)
|
||||||
|
}}/>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th style={{width: '30%'}}>Description</th>
|
||||||
|
<th>Version</th>
|
||||||
|
<th>Last updated</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody style={{overflow: 'auto'}}>
|
||||||
|
{plugins.map((plugin, index) => {
|
||||||
|
return <tr key={index}>
|
||||||
|
<td><a href={`https://npmjs.com/${plugin.name}`} target="_blank">{plugin.name}</a></td>
|
||||||
|
<td>{plugin.description}</td>
|
||||||
|
<td>{plugin.version}</td>
|
||||||
|
<td>{plugin.time}</td>
|
||||||
|
<td>
|
||||||
|
<button onClick={() => installPlugin(plugin.name)}>Installieren</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,3 +5,21 @@ export type PluginDef = {
|
||||||
time: string,
|
time: string,
|
||||||
official: boolean,
|
official: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type InstalledPlugin = {
|
||||||
|
name: string,
|
||||||
|
path: string,
|
||||||
|
realPath: string,
|
||||||
|
version:string,
|
||||||
|
updatable: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type SearchParams = {
|
||||||
|
searchTerm: string,
|
||||||
|
offset: number,
|
||||||
|
limit: number,
|
||||||
|
sortBy: 'name'|'version',
|
||||||
|
sortDir: 'asc'|'desc'
|
||||||
|
}
|
|
@ -15,8 +15,18 @@ export const SettingsPage = ()=>{
|
||||||
if (isJSONClean(settings!)) {
|
if (isJSONClean(settings!)) {
|
||||||
// JSON is clean so emit it to the server
|
// JSON is clean so emit it to the server
|
||||||
settingsSocket!.emit('saveSettings', settings!);
|
settingsSocket!.emit('saveSettings', settings!);
|
||||||
|
useStore.getState().setToastState({
|
||||||
|
open: true,
|
||||||
|
title: "Succesfully saved settings",
|
||||||
|
success: true
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log('Invalid JSON');
|
console.log('Invalid JSON');
|
||||||
|
useStore.getState().setToastState({
|
||||||
|
open: true,
|
||||||
|
title: "Error saving settings",
|
||||||
|
success: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}}>Einstellungen speichern</button>
|
}}>Einstellungen speichern</button>
|
||||||
<button className="settingsButton" onClick={()=>{
|
<button className="settingsButton" onClick={()=>{
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import {create} from "zustand";
|
import {create} from "zustand";
|
||||||
import {Socket} from "socket.io-client";
|
import {Socket} from "socket.io-client";
|
||||||
|
|
||||||
|
type ToastState = {
|
||||||
|
description?:string,
|
||||||
|
title: string,
|
||||||
|
open: boolean,
|
||||||
|
success: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type StoreState = {
|
type StoreState = {
|
||||||
settings: string|undefined,
|
settings: string|undefined,
|
||||||
setSettings: (settings: string) => void,
|
setSettings: (settings: string) => void,
|
||||||
|
@ -9,7 +17,9 @@ type StoreState = {
|
||||||
showLoading: boolean,
|
showLoading: boolean,
|
||||||
setShowLoading: (show: boolean) => void,
|
setShowLoading: (show: boolean) => void,
|
||||||
setPluginsSocket: (socket: Socket) => void
|
setPluginsSocket: (socket: Socket) => void
|
||||||
pluginsSocket: Socket|undefined
|
pluginsSocket: Socket|undefined,
|
||||||
|
toastState: ToastState,
|
||||||
|
setToastState: (val: ToastState)=>void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,5 +31,12 @@ export const useStore = create<StoreState>()((set) => ({
|
||||||
showLoading: false,
|
showLoading: false,
|
||||||
setShowLoading: (show: boolean) => set({showLoading: show}),
|
setShowLoading: (show: boolean) => set({showLoading: show}),
|
||||||
pluginsSocket: undefined,
|
pluginsSocket: undefined,
|
||||||
setPluginsSocket: (socket: Socket) => set({pluginsSocket: socket})
|
setPluginsSocket: (socket: Socket) => set({pluginsSocket: socket}),
|
||||||
|
setToastState: (val )=>set({toastState: val}),
|
||||||
|
toastState: {
|
||||||
|
open: false,
|
||||||
|
title: '',
|
||||||
|
description:'',
|
||||||
|
success: false
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
29
admin/src/utils/AnimationFrameHook.ts
Normal file
29
admin/src/utils/AnimationFrameHook.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import {useCallback, useEffect, useRef} from "react";
|
||||||
|
|
||||||
|
type Args = any[]
|
||||||
|
|
||||||
|
export const useAnimationFrame = <Fn extends (...args: Args)=>void>(
|
||||||
|
callback: Fn,
|
||||||
|
wait = 0
|
||||||
|
): ((...args: Parameters<Fn>)=>void)=>{
|
||||||
|
const rafId = useRef(0)
|
||||||
|
const render = useCallback(
|
||||||
|
(...args: Parameters<Fn>)=>{
|
||||||
|
cancelAnimationFrame(rafId.current)
|
||||||
|
const timeStart = performance.now()
|
||||||
|
|
||||||
|
const renderFrame = (timeNow: number)=>{
|
||||||
|
if(timeNow-timeStart<wait){
|
||||||
|
rafId.current = requestAnimationFrame(renderFrame)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callback(...args)
|
||||||
|
}
|
||||||
|
rafId.current = requestAnimationFrame(renderFrame)
|
||||||
|
}, [callback, wait]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(()=>cancelAnimationFrame(rafId.current),[])
|
||||||
|
return render
|
||||||
|
}
|
26
admin/src/utils/Toast.tsx
Normal file
26
admin/src/utils/Toast.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import * as Toast from '@radix-ui/react-toast'
|
||||||
|
import {useStore} from "../store/store.ts";
|
||||||
|
import {useMemo} from "react";
|
||||||
|
|
||||||
|
export const ToastDialog = ()=>{
|
||||||
|
const toastState = useStore(state => state.toastState)
|
||||||
|
const resultingClass = useMemo(()=> {
|
||||||
|
return toastState.success?'ToastRootSuccess':'ToastRootFailure'
|
||||||
|
}, [toastState.success])
|
||||||
|
|
||||||
|
console.log()
|
||||||
|
return <>
|
||||||
|
<Toast.Root className={"ToastRoot "+resultingClass} open={toastState && toastState.open} onOpenChange={()=>{
|
||||||
|
useStore.getState().setToastState({
|
||||||
|
...toastState!,
|
||||||
|
open: !toastState?.open
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<Toast.Title className="ToastTitle">{toastState.title}</Toast.Title>
|
||||||
|
<Toast.Description asChild>
|
||||||
|
{toastState.description}
|
||||||
|
</Toast.Description>
|
||||||
|
</Toast.Root>
|
||||||
|
<Toast.Viewport className="ToastViewport"/>
|
||||||
|
</>
|
||||||
|
}
|
22
admin/src/utils/useDebounce.ts
Normal file
22
admin/src/utils/useDebounce.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {DependencyList, EffectCallback, useMemo, useRef} from "react";
|
||||||
|
import {useAnimationFrame} from "./AnimationFrameHook";
|
||||||
|
|
||||||
|
const defaultDeps: DependencyList = []
|
||||||
|
|
||||||
|
export const useDebounce = (
|
||||||
|
fn:EffectCallback,
|
||||||
|
wait = 0,
|
||||||
|
deps = defaultDeps
|
||||||
|
):void => {
|
||||||
|
const isFirstRender = useRef(true)
|
||||||
|
const render = useAnimationFrame(fn, wait)
|
||||||
|
|
||||||
|
useMemo(()=>{
|
||||||
|
if(isFirstRender.current){
|
||||||
|
isFirstRender.current = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
render()
|
||||||
|
}, deps)
|
||||||
|
}
|
|
@ -1,10 +1,19 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
import svgr from 'vite-plugin-svgr'
|
import svgr from 'vite-plugin-svgr'
|
||||||
|
import {viteStaticCopy} from "vite-plugin-static-copy";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), svgr()],
|
plugins: [react(), svgr(), viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: '../src/locales',
|
||||||
|
dest: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})],
|
||||||
|
base: '/admin',
|
||||||
server:{
|
server:{
|
||||||
proxy: {
|
proxy: {
|
||||||
'/socket.io/': {
|
'/socket.io/': {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue