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:
SamTV12345 2024-03-09 23:07:09 +01:00 committed by GitHub
parent d34b964cc2
commit db46ffb63b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
112 changed files with 3327 additions and 946 deletions

View 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
}

View 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>
}

View 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
View 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
View 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

View 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';
}

View 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
View 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
}
};