From 7bc65ad01a3dd676bc411ceefe0191c30efbf29e Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Sat, 27 Apr 2024 15:19:20 +0300 Subject: [PATCH] Add frontend admin tools and tune animations --- .../src/app/Navigation/UserDropdown.tsx | 26 +++++++++--- .../frontend/src/app/Navigation/UserMenu.tsx | 41 ++++++++++++++----- rsconcept/frontend/src/app/urls.ts | 3 ++ rsconcept/frontend/src/components/Icons.tsx | 5 ++- .../frontend/src/components/ui/Loader.tsx | 11 +++-- .../frontend/src/context/AuthContext.tsx | 17 ++++---- .../frontend/src/context/OptionsContext.tsx | 6 +++ .../src/pages/RSFormPage/RSEditContext.tsx | 8 ++-- rsconcept/frontend/src/styling/animations.ts | 6 +-- 9 files changed, 90 insertions(+), 33 deletions(-) diff --git a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx index e652ac83..859c68b3 100644 --- a/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserDropdown.tsx @@ -1,6 +1,6 @@ -import { LuLogOut, LuMoon, LuSun } from 'react-icons/lu'; +import { LuMoon, LuSun } from 'react-icons/lu'; -import { IconHelp, IconHelpOff, IconUser } from '@/components/Icons'; +import { IconAdmin, IconAdminOff, IconDatabase, IconHelp, IconHelpOff, IconLogout, IconUser } from '@/components/Icons'; import { CProps } from '@/components/props'; import Dropdown from '@/components/ui/Dropdown'; import DropdownButton from '@/components/ui/DropdownButton'; @@ -16,7 +16,7 @@ interface UserDropdownProps { } function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { - const { darkMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions(); + const { darkMode, adminMode, toggleAdminMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions(); const router = useConceptNavigation(); const { user, logout } = useAuth(); @@ -30,13 +30,18 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { logout(() => router.push(urls.login)); } + function gotoAdmin() { + hideDropdown(); + logout(() => router.push(urls.admin, true)); + } + function handleToggleDarkMode() { hideDropdown(); toggleDarkMode(); } return ( - + + {user?.is_staff ? ( + : } + title='Работа в режиме администратора' + onClick={toggleAdminMode} + /> + ) : null} + {user?.is_staff ? ( + } onClick={gotoAdmin} /> + ) : null} } + icon={} onClick={logoutAndRedirect} /> diff --git a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx index 21b42685..38b2989d 100644 --- a/rsconcept/frontend/src/app/Navigation/UserMenu.tsx +++ b/rsconcept/frontend/src/app/Navigation/UserMenu.tsx @@ -1,8 +1,12 @@ +import { AnimatePresence } from 'framer-motion'; import { FaCircleUser } from 'react-icons/fa6'; import { IconLogin } from '@/components/Icons'; +import Loader from '@/components/ui/Loader'; +import AnimateFade from '@/components/wrap/AnimateFade'; import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptOptions } from '@/context/OptionsContext'; import useDropdown from '@/hooks/useDropdown'; import { urls } from '../urls'; @@ -11,20 +15,37 @@ import UserDropdown from './UserDropdown'; function UserMenu() { const router = useConceptNavigation(); - const { user } = useAuth(); + const { user, loading } = useAuth(); + const { adminMode } = useConceptOptions(); const menu = useDropdown(); const navigateLogin = () => router.push(urls.login); return ( -
- {!user ? ( - } - onClick={navigateLogin} - /> - ) : null} - {user ? } onClick={menu.toggle} /> : null} +
+ + {loading ? ( + + + + ) : null} + {!user && !loading ? ( + + } + onClick={navigateLogin} + /> + + ) : null} + {user ? ( + + } + onClick={menu.toggle} + /> + + ) : null} + menu.hide()} />
); diff --git a/rsconcept/frontend/src/app/urls.ts b/rsconcept/frontend/src/app/urls.ts index c4e691c8..db0df19b 100644 --- a/rsconcept/frontend/src/app/urls.ts +++ b/rsconcept/frontend/src/app/urls.ts @@ -2,6 +2,8 @@ * Module: Internal navigation constants. */ +import { buildConstants } from '@/utils/constants'; + /** * Routes. */ @@ -29,6 +31,7 @@ interface SchemaProps { * Internal navigation URLs. */ export const urls = { + admin: `${buildConstants.backend}/admin`, home: '/', login: `/${routes.login}`, login_hint: (userName: string) => `/login?username=${userName}`, diff --git a/rsconcept/frontend/src/components/Icons.tsx b/rsconcept/frontend/src/components/Icons.tsx index 94843a01..e6030e13 100644 --- a/rsconcept/frontend/src/components/Icons.tsx +++ b/rsconcept/frontend/src/components/Icons.tsx @@ -1,5 +1,6 @@ // Search new icons at https://reactsvgicons.com/ +export { LuLogOut as IconLogout } from 'react-icons/lu'; export { FiSave as IconSave } from 'react-icons/fi'; export { BiCheck as IconAccept } from 'react-icons/bi'; export { BiX as IconClose } from 'react-icons/bi'; @@ -17,7 +18,8 @@ export { RiUnpinLine as IconUnpin } from 'react-icons/ri'; export { BiCog as IconSettings } from 'react-icons/bi'; export { LuUserCircle2 as IconUser } from 'react-icons/lu'; export { LuCrown as IconOwner } from 'react-icons/lu'; -export { BiMeteor as IconAdmin } from 'react-icons/bi'; +export { TbMeteor as IconAdmin } from 'react-icons/tb'; +export { TbMeteorOff as IconAdminOff } from 'react-icons/tb'; export { LuGlasses as IconReader } from 'react-icons/lu'; export { FiBell as IconFollow } from 'react-icons/fi'; export { FiBellOff as IconFollowOff } from 'react-icons/fi'; @@ -26,6 +28,7 @@ export { FaSortAmountDownAlt as IconSortText } from 'react-icons/fa'; export { LuChevronDown as IconDropArrow } from 'react-icons/lu'; export { LuChevronUp as IconDropArrowUp } from 'react-icons/lu'; +export { LuDatabase as IconDatabase } from 'react-icons/lu'; export { LuImage as IconImage } from 'react-icons/lu'; export { TbColumns as IconList } from 'react-icons/tb'; export { TbColumnsOff as IconListOff } from 'react-icons/tb'; diff --git a/rsconcept/frontend/src/components/ui/Loader.tsx b/rsconcept/frontend/src/components/ui/Loader.tsx index 48ddb187..91731e98 100644 --- a/rsconcept/frontend/src/components/ui/Loader.tsx +++ b/rsconcept/frontend/src/components/ui/Loader.tsx @@ -1,6 +1,6 @@ 'use client'; -import { ThreeDots } from 'react-loader-spinner'; +import { ThreeCircles, ThreeDots } from 'react-loader-spinner'; import { useConceptOptions } from '@/context/OptionsContext'; @@ -8,13 +8,18 @@ import AnimateFade from '../wrap/AnimateFade'; interface LoaderProps { size?: number; + circular?: boolean; } -function Loader({ size = 10 }: LoaderProps) { +function Loader({ size = 10, circular }: LoaderProps) { const { colors } = useConceptOptions(); return ( - + {circular ? ( + + ) : ( + + )} ); } diff --git a/rsconcept/frontend/src/context/AuthContext.tsx b/rsconcept/frontend/src/context/AuthContext.tsx index ae9c1cec..f6df8fa3 100644 --- a/rsconcept/frontend/src/context/AuthContext.tsx +++ b/rsconcept/frontend/src/context/AuthContext.tsx @@ -2,13 +2,6 @@ import { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react'; -import { type ErrorData } from '@/components/info/InfoError'; -import { IPasswordTokenData, IRequestPasswordData, IResetPasswordData, IUserLoginData } from '@/models/library'; -import { ICurrentUser } from '@/models/library'; -import { IUserSignupData } from '@/models/library'; -import { IUserProfile } from '@/models/library'; -import { IUserInfo } from '@/models/library'; -import { IUserUpdatePassword } from '@/models/library'; import { type DataCallback, getAuth, @@ -20,6 +13,13 @@ import { postSignup, postValidatePasswordToken } from '@/app/backendAPI'; +import { type ErrorData } from '@/components/info/InfoError'; +import { IPasswordTokenData, IRequestPasswordData, IResetPasswordData, IUserLoginData } from '@/models/library'; +import { ICurrentUser } from '@/models/library'; +import { IUserSignupData } from '@/models/library'; +import { IUserProfile } from '@/models/library'; +import { IUserInfo } from '@/models/library'; +import { IUserUpdatePassword } from '@/models/library'; import { useUsers } from './UsersContext'; @@ -53,13 +53,14 @@ interface AuthStateProps { export const AuthState = ({ children }: AuthStateProps) => { const { users } = useUsers(); const [user, setUser] = useState(undefined); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(true); const [error, setError] = useState(undefined); const reload = useCallback( (callback?: () => void) => { getAuth({ onError: () => setUser(undefined), + setLoading: setLoading, onSuccess: currentUser => { if (currentUser.id) { setUser(currentUser); diff --git a/rsconcept/frontend/src/context/OptionsContext.tsx b/rsconcept/frontend/src/context/OptionsContext.tsx index 911f51e0..c171daa3 100644 --- a/rsconcept/frontend/src/context/OptionsContext.tsx +++ b/rsconcept/frontend/src/context/OptionsContext.tsx @@ -19,6 +19,9 @@ interface IOptionsContext { darkMode: boolean; toggleDarkMode: () => void; + adminMode: boolean; + toggleAdminMode: () => void; + mathFont: FontStyle; setMathFont: (value: FontStyle) => void; @@ -53,6 +56,7 @@ interface OptionsStateProps { export const OptionsState = ({ children }: OptionsStateProps) => { const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false); + const [adminMode, setAdminMode] = useLocalStorage(storage.themeDark, false); const [mathFont, setMathFont] = useLocalStorage(storage.rseditFont, 'math'); const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true); const [noNavigation, setNoNavigation] = useState(false); @@ -121,6 +125,7 @@ export const OptionsState = ({ children }: OptionsStateProps) => { { showScroll, showHelp, toggleDarkMode: toggleDarkMode, + toggleAdminMode: () => setAdminMode(prev => !prev), toggleNoNavigation: toggleNoNavigation, setNoFooter, setShowScroll, diff --git a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx index d43f28ca..021f39ef 100644 --- a/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx +++ b/rsconcept/frontend/src/pages/RSFormPage/RSEditContext.tsx @@ -14,6 +14,7 @@ import TextURL from '@/components/ui/TextURL'; import { useAccessMode } from '@/context/AccessModeContext'; import { useAuth } from '@/context/AuthContext'; import { useConceptNavigation } from '@/context/NavigationContext'; +import { useConceptOptions } from '@/context/OptionsContext'; import { useRSForm } from '@/context/RSFormContext'; import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem'; import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate'; @@ -120,6 +121,7 @@ export const RSEditState = ({ }: RSEditStateProps) => { const router = useConceptNavigation(); const { user } = useAuth(); + const { adminMode } = useConceptOptions(); const { mode, setMode } = useAccessMode(); const model = useRSForm(); @@ -152,15 +154,15 @@ export const RSEditState = ({ useLayoutEffect( () => setMode(prev => { - if (prev === UserAccessMode.ADMIN) { - return prev; + if (user?.is_staff && (prev === UserAccessMode.ADMIN || adminMode)) { + return UserAccessMode.ADMIN; } else if (model.isOwned) { return UserAccessMode.OWNER; } else { return UserAccessMode.READER; } }), - [model.schema, setMode, model.isOwned] + [model.schema, setMode, model.isOwned, user, adminMode] ); const viewVersion = useCallback( diff --git a/rsconcept/frontend/src/styling/animations.ts b/rsconcept/frontend/src/styling/animations.ts index 5282c28b..4a3855fd 100644 --- a/rsconcept/frontend/src/styling/animations.ts +++ b/rsconcept/frontend/src/styling/animations.ts @@ -243,7 +243,7 @@ export const animateFade = { transition: { type: 'tween', ease: 'linear', - duration: 0.3 + duration: 0.4 } }, hidden: { @@ -251,7 +251,7 @@ export const animateFade = { transition: { type: 'tween', ease: 'linear', - duration: 0.3 + duration: 0.4 } } }, @@ -260,7 +260,7 @@ export const animateFade = { transition: { type: 'tween', ease: 'linear', - duration: 0.3 + duration: 0.4 } } };