mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-06-14 10:14:45 -04:00
feat(admin): Added shoutout to admin panel (#6346)
* Added shoutout * Added shoutout function * Fixed test. * Included feedback from review. * Removed unnecessary file
This commit is contained in:
parent
d64924e9f5
commit
e12be96102
14 changed files with 467 additions and 214 deletions
|
@ -9,19 +9,12 @@
|
|||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-switch": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"lucide-react": "^0.372.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"zustand": "^4.5.2",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
||||
|
@ -30,10 +23,19 @@
|
|||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"i18next": "^23.11.2",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"lucide-react": "^0.372.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-i18next": "^14.1.0",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.9",
|
||||
"vite-plugin-static-copy": "^1.0.3",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"zustand": "^4.5.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {NavLink, Outlet, useNavigate} from "react-router-dom";
|
|||
import {useStore} from "./store/store.ts";
|
||||
import {LoadingScreen} from "./utils/LoadingScreen.tsx";
|
||||
import {Trans, useTranslation} from "react-i18next";
|
||||
import {Cable, Construction, Crown, NotepadText, Wrench} from "lucide-react";
|
||||
import {Cable, Construction, Crown, NotepadText, Wrench, PhoneCall} from "lucide-react";
|
||||
|
||||
const WS_URL = import.meta.env.DEV? 'http://localhost:9001' : ''
|
||||
export const App = ()=> {
|
||||
|
@ -96,8 +96,10 @@ export const App = ()=> {
|
|||
<ul>
|
||||
<li><NavLink to="/plugins"><Cable/><Trans i18nKey="admin_plugins"/></NavLink></li>
|
||||
<li><NavLink to={"/settings"}><Wrench/><Trans i18nKey="admin_settings"/></NavLink></li>
|
||||
<li> <NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
|
||||
<li><NavLink to={"/pads"}><NotepadText/><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
||||
<li><NavLink to={"/help"}> <Construction/> <Trans i18nKey="admin_plugins_info"/></NavLink></li>
|
||||
<li><NavLink to={"/pads"}><NotepadText/><Trans
|
||||
i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></NavLink></li>
|
||||
<li><NavLink to={"/shout"}><PhoneCall/>Communication</NavLink></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
13
admin/src/components/ShoutType.ts
Normal file
13
admin/src/components/ShoutType.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export type ShoutType = {
|
||||
type: string,
|
||||
data:{
|
||||
type: string,
|
||||
payload: {
|
||||
message: {
|
||||
message: string,
|
||||
sticky: boolean
|
||||
},
|
||||
timestamp: number
|
||||
}
|
||||
}
|
||||
}
|
|
@ -604,6 +604,25 @@ pre {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
.send-message {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.send-message input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.send-message {
|
||||
}
|
||||
|
||||
.send-message svg {
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: -3px;
|
||||
left: auto !important;
|
||||
}
|
||||
|
||||
.search-field svg {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
|
@ -733,3 +752,52 @@ input, button, select, optgroup, textarea {
|
|||
right: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.SwitchRoot {
|
||||
align-self: center;
|
||||
width: 60px;
|
||||
height: 30px;
|
||||
background-color: black;
|
||||
border-radius: 9999px;
|
||||
position: relative;
|
||||
box-shadow: 0 2px 10px var(--black-a7);
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
.SwitchRoot:focus {
|
||||
box-shadow: 0 0 0 2px black;
|
||||
}
|
||||
.SwitchRoot[data-state='checked'] {
|
||||
background-color: var(--etherpad-color);
|
||||
}
|
||||
|
||||
.SwitchThumb {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: white;
|
||||
border-radius: 9999px;
|
||||
box-shadow: 0 2px 2px var(--black-a7);
|
||||
transition: transform 100ms;
|
||||
transform: translateX(2px);
|
||||
will-change: transform;
|
||||
}
|
||||
.SwitchThumb[data-state='checked'] {
|
||||
transform: translateX(25px);
|
||||
}
|
||||
|
||||
.Label {
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.message {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
border: 1px solid #e0e0e0;
|
||||
margin: 10px 20px 10px 10px;
|
||||
border-radius: 10px 0 10px 10px;
|
||||
background-color: var(--etherpad-color);
|
||||
color: white
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import {I18nextProvider} from "react-i18next";
|
|||
import i18n from "./localization/i18n.ts";
|
||||
import {PadPage} from "./pages/PadPage.tsx";
|
||||
import {ToastDialog} from "./utils/Toast.tsx";
|
||||
import {ShoutPage} from "./pages/ShoutPage.tsx";
|
||||
|
||||
const router = createBrowserRouter(createRoutesFromElements(
|
||||
<><Route element={<App/>}>
|
||||
|
@ -20,6 +21,7 @@ const router = createBrowserRouter(createRoutesFromElements(
|
|||
<Route path="/settings" element={<SettingsPage/>}/>
|
||||
<Route path="/help" element={<HelpPage/>}/>
|
||||
<Route path="/pads" element={<PadPage/>}/>
|
||||
<Route path="/shout" element={<ShoutPage/>}/>
|
||||
</Route><Route path="/login">
|
||||
<Route index element={<LoginScreen/>}/>
|
||||
</Route></>
|
||||
|
|
76
admin/src/pages/ShoutPage.tsx
Normal file
76
admin/src/pages/ShoutPage.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {useEffect, useState} from "react";
|
||||
import {SendHorizonal} from 'lucide-react'
|
||||
import {useStore} from "../store/store.ts";
|
||||
import * as Switch from '@radix-ui/react-switch';
|
||||
import {ShoutType} from "../components/ShoutType.ts";
|
||||
|
||||
export const ShoutPage = ()=>{
|
||||
const [totalUsers, setTotalUsers] = useState(0);
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [sticky, setSticky] = useState<boolean>(false);
|
||||
const socket = useStore(state => state.settingsSocket);
|
||||
const [shouts, setShouts] = useState<ShoutType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/stats')
|
||||
.then(response => response.json())
|
||||
.then(data => setTotalUsers(data.totalUsers));
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if(socket) {
|
||||
socket.on('shout', (shout) => {
|
||||
setShouts([...shouts, shout])
|
||||
})
|
||||
}
|
||||
}, [socket, shouts])
|
||||
|
||||
const sendMessage = () => {
|
||||
socket?.emit('shout', {
|
||||
message,
|
||||
sticky
|
||||
});
|
||||
setMessage('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Communication</h1>
|
||||
{totalUsers > 0 && <p>There {totalUsers>1?"are":"is"} currently {totalUsers} user{totalUsers>1?"s":""} online</p>}
|
||||
<div style={{height: '80vh', display: 'flex', flexDirection: 'column'}}>
|
||||
<div style={{flexGrow: 1, backgroundColor: 'white', overflowY: "auto"}}>
|
||||
{
|
||||
shouts.map((shout) => {
|
||||
return (
|
||||
<div key={shout.data.payload.timestamp} className="message">
|
||||
<div>{shout.data.payload.message.message}</div>
|
||||
<div style={{display: 'flex'}}>
|
||||
<div style={{flexGrow: 1}}></div>
|
||||
<div
|
||||
style={{color: "lightgray"}}>{new Date(shout.data.payload.timestamp).toLocaleTimeString()
|
||||
+ " " + new Date(shout.data.payload.timestamp).toLocaleDateString()}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
sendMessage()
|
||||
}} className="send-message search-field" style={{display: 'flex', gap: '10px'}}>
|
||||
<Switch.Root title="Change sticky message" className="SwitchRoot" checked={sticky}
|
||||
onCheckedChange={() => {
|
||||
setSticky(!sticky);
|
||||
}}>
|
||||
<Switch.Thumb className="SwitchThumb"/>
|
||||
</Switch.Root>
|
||||
<input required value={message} onChange={v=>setMessage(v.target.value)}
|
||||
style={{width: '100%', paddingRight: '55px', backgroundColor: '#e0e0e0', flexGrow: 1}}/>
|
||||
<SendHorizonal style={{bottom: '5px', right: '9px', color: '#0f775b'}} onClick={()=>sendMessage()}/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -28,8 +28,11 @@ export default defineConfig({
|
|||
'/admin-auth/': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/admin-prox/, '/admin/')
|
||||
},
|
||||
'/stats': {
|
||||
target: 'http://localhost:9001',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue