mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-25 18:06:15 -04:00
Feat/admin react (#6211)
* Added vite react admin ui. * Added react i18next. * Added pads manager. * Fixed docker build. * Fixed windows build. * Fixed installOnWindows script. * Install only if path exists.
This commit is contained in:
parent
d34b964cc2
commit
db46ffb63b
112 changed files with 3327 additions and 946 deletions
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
|
||||
}
|
19
admin/src/utils/LoadingScreen.tsx
Normal file
19
admin/src/utils/LoadingScreen.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {useStore} from "../store/store.ts";
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import ReactComponent from './brand.svg?react';
|
||||
export const LoadingScreen = ()=>{
|
||||
const showLoading = useStore(state => state.showLoading)
|
||||
|
||||
return <Dialog.Root open={showLoading}><Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
|
||||
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
|
||||
<div className="mt-4 text-[--fg-color]">
|
||||
<ReactComponent/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
}
|
20
admin/src/utils/PadSearch.ts
Normal file
20
admin/src/utils/PadSearch.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export type PadSearchQuery = {
|
||||
pattern: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
ascending: boolean;
|
||||
sortBy: string;
|
||||
}
|
||||
|
||||
|
||||
export type PadSearchResult = {
|
||||
total: number;
|
||||
results?: PadType[]
|
||||
}
|
||||
|
||||
export type PadType = {
|
||||
padName: string;
|
||||
lastEdited: number;
|
||||
userCount: number;
|
||||
revisionNumber: number;
|
||||
}
|
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"/>
|
||||
</>
|
||||
}
|
50
admin/src/utils/brand.svg
Normal file
50
admin/src/utils/brand.svg
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill="#0f775b" width="355px" height="355px" viewBox="0 0 355 355" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Group 10</title>
|
||||
<defs>
|
||||
<!-- top line -->
|
||||
<rect id="path-4" x="41" y="110" width="142" height="25" rx="12.5" fill="#0f775b">
|
||||
<animate attributeName="width" from="0" to="142" dur="3s" fill="freeze"/>
|
||||
</rect>
|
||||
|
||||
<!-- middle line -->
|
||||
<rect id="path-2" x="42" y="167" width="168" height="27" rx="13.5" fill="#0f775b">
|
||||
<animate attributeName="width" from="0" to="168" dur="5s" fill="freeze"/>
|
||||
</rect>
|
||||
|
||||
<!-- bottom line -->
|
||||
<rect id="path-6" x="41" y="226" width="105" height="25" rx="12.5" fill="#0f775b">
|
||||
<animate attributeName="width" from="0" to="105" dur="2s" fill="freeze"/>
|
||||
</rect>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" >
|
||||
<g id="Group-5-Copy-2" transform="translate(-415.000000, -351.000000)">
|
||||
<g id="Group-10" transform="translate(415.000000, 351.000000)">
|
||||
<g id="Group-9" transform="translate(0.000000, 15.000000)">
|
||||
<!-- small radio wave -->
|
||||
<path stroke="0f775b" d="M237.612214,138.157654 C234.725783,135.28192 230.051254,135.279644 227.164823,138.157654 C224.278392,141.035663 224.278392,145.698831 227.164823,148.57684 C234.93988,156.329214 239.222735,166.601382 239.222735,177.499403 C239.222735,188.397423 234.93988,198.669591 227.164823,206.424696 C224.278392,209.30043 224.278392,213.965873 227.164823,216.841607 C228.608267,218.280384 230.497251,219 232.388518,219 C234.277503,219 236.16877,218.280384 237.612214,216.841607 C248.18012,206.304532 254,192.334147 254,177.499403 C254,162.665114 248.18012,148.694728 237.612214,138.157654 Z" id="Path-Copy-26" fill-opacity="0.200482" fill="#000000" fill-rule="nonzero" opacity="0.754065225">
|
||||
<animate attributeName="opacity" from="0" to="1" dur="3s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
<!-- large radio wave -->
|
||||
<path stroke="0f775b" d="M267.333026,113.158661 C264.51049,110.280446 259.939438,110.280446 257.116902,113.158661 C254.294366,116.039154 254.294366,120.709078 257.116902,123.586837 C285.703837,152.763042 285.703837,200.237641 257.116902,229.413847 C254.294366,232.292061 254.294366,236.96153 257.116902,239.839744 C258.528393,241.280219 260.375562,242 262.224964,242 C264.074365,242 265.921535,241.279763 267.333026,239.837011 C301.555658,204.912576 301.555658,148.084007 267.333026,113.158661 Z" id="Path-Copy-27" fill-opacity="0.250565" fill="#131514" fill-rule="nonzero" opacity="0.754065225">
|
||||
<animate attributeName="opacity" from="0" to="1" dur="3s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
<!-- top line -->
|
||||
<g stroke="0f775b" id="Rectangle-Copy-56">
|
||||
<use fill="#000000" fill-opacity="0.200482" fill-rule="evenodd" xlink:href="#path-4"></use>
|
||||
</g>
|
||||
<!-- middle line -->
|
||||
<g stroke="0f775b" id="Rectangle-Copy-55">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-3)" xlink:href="#path-2"></use>
|
||||
<use fill="#000000" fill-opacity="0.200482" fill-rule="evenodd" xlink:href="#path-2"></use>
|
||||
</g>
|
||||
<!-- bottom line -->
|
||||
<g stroke="0f775b" id="Rectangle-Copy-57">
|
||||
<use fill="black" fill-opacity="1" filter="url(#filter-7)" xlink:href="#path-6"></use>
|
||||
<use fill="#000000" fill-opacity="0.200482" xlink:href="#path-6"></use>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.8 KiB |
6
admin/src/utils/sorting.ts
Normal file
6
admin/src/utils/sorting.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const determineSorting = (sortBy: string, ascending: boolean, currentSymbol: string) => {
|
||||
if (sortBy === currentSymbol) {
|
||||
return ascending ? 'sort up' : 'sort down';
|
||||
}
|
||||
return 'sort none';
|
||||
}
|
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)
|
||||
}
|
64
admin/src/utils/utils.ts
Normal file
64
admin/src/utils/utils.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
const minify = (json: string)=>{
|
||||
|
||||
let tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
|
||||
in_string = false,
|
||||
in_multiline_comment = false,
|
||||
in_singleline_comment = false,
|
||||
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
|
||||
;
|
||||
|
||||
tokenizer.lastIndex = 0;
|
||||
|
||||
while (tmp = tokenizer.exec(json)) {
|
||||
lc = RegExp.leftContext;
|
||||
rc = RegExp.rightContext;
|
||||
if (!in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.substring(from);
|
||||
if (!in_string) {
|
||||
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
|
||||
}
|
||||
new_str[ns++] = tmp2;
|
||||
}
|
||||
from = tokenizer.lastIndex;
|
||||
|
||||
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.match(/(\\)*$/);
|
||||
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
|
||||
in_string = !in_string;
|
||||
}
|
||||
from--; // include " character in next catch
|
||||
rc = json.substring(from);
|
||||
}
|
||||
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = true;
|
||||
}
|
||||
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = false;
|
||||
}
|
||||
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_singleline_comment = true;
|
||||
}
|
||||
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
|
||||
in_singleline_comment = false;
|
||||
}
|
||||
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
|
||||
new_str[ns++] = tmp[0];
|
||||
}
|
||||
}
|
||||
new_str[ns++] = rc;
|
||||
return new_str.join("");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const isJSONClean = (data: string) => {
|
||||
let cleanSettings = minify(data);
|
||||
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
||||
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
||||
try {
|
||||
return typeof JSON.parse(cleanSettings) === 'object';
|
||||
} catch (e) {
|
||||
return false; // the JSON failed to be parsed
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue