Add frontend admin tools and tune animations

This commit is contained in:
IRBorisov 2024-04-27 15:19:20 +03:00
parent 128bdf5ec4
commit 7bc65ad01a
9 changed files with 90 additions and 33 deletions

View File

@ -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 { CProps } from '@/components/props';
import Dropdown from '@/components/ui/Dropdown'; import Dropdown from '@/components/ui/Dropdown';
import DropdownButton from '@/components/ui/DropdownButton'; import DropdownButton from '@/components/ui/DropdownButton';
@ -16,7 +16,7 @@ interface UserDropdownProps {
} }
function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) { function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
const { darkMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions(); const { darkMode, adminMode, toggleAdminMode, toggleDarkMode, showHelp, toggleShowHelp } = useConceptOptions();
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user, logout } = useAuth(); const { user, logout } = useAuth();
@ -30,13 +30,18 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
logout(() => router.push(urls.login)); logout(() => router.push(urls.login));
} }
function gotoAdmin() {
hideDropdown();
logout(() => router.push(urls.admin, true));
}
function handleToggleDarkMode() { function handleToggleDarkMode() {
hideDropdown(); hideDropdown();
toggleDarkMode(); toggleDarkMode();
} }
return ( return (
<Dropdown className='min-w-[18ch] max-w-[12rem]' stretchLeft isOpen={isOpen}> <Dropdown className='mt-[1.5rem] min-w-[18ch] max-w-[12rem]' stretchLeft isOpen={isOpen}>
<DropdownButton <DropdownButton
text={user?.username} text={user?.username}
title='Профиль пользователя' title='Профиль пользователя'
@ -55,10 +60,21 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
title='Отображение иконок подсказок' title='Отображение иконок подсказок'
onClick={toggleShowHelp} onClick={toggleShowHelp}
/> />
{user?.is_staff ? (
<DropdownButton
text={adminMode ? 'Админ: Вкл' : 'Админ: Выкл'}
icon={adminMode ? <IconAdmin size='1rem' /> : <IconAdminOff size='1rem' />}
title='Работа в режиме администратора'
onClick={toggleAdminMode}
/>
) : null}
{user?.is_staff ? (
<DropdownButton text='База данных' icon={<IconDatabase size='1rem' />} onClick={gotoAdmin} />
) : null}
<DropdownButton <DropdownButton
text='Выйти...' text='Выйти...'
className='font-semibold' className='font-semibold'
icon={<LuLogOut size='1rem' />} icon={<IconLogout size='1rem' />}
onClick={logoutAndRedirect} onClick={logoutAndRedirect}
/> />
</Dropdown> </Dropdown>

View File

@ -1,8 +1,12 @@
import { AnimatePresence } from 'framer-motion';
import { FaCircleUser } from 'react-icons/fa6'; import { FaCircleUser } from 'react-icons/fa6';
import { IconLogin } from '@/components/Icons'; import { IconLogin } from '@/components/Icons';
import Loader from '@/components/ui/Loader';
import AnimateFade from '@/components/wrap/AnimateFade';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext';
import useDropdown from '@/hooks/useDropdown'; import useDropdown from '@/hooks/useDropdown';
import { urls } from '../urls'; import { urls } from '../urls';
@ -11,20 +15,37 @@ import UserDropdown from './UserDropdown';
function UserMenu() { function UserMenu() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user, loading } = useAuth();
const { adminMode } = useConceptOptions();
const menu = useDropdown(); const menu = useDropdown();
const navigateLogin = () => router.push(urls.login); const navigateLogin = () => router.push(urls.login);
return ( return (
<div ref={menu.ref} className='h-full'> <div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
{!user ? ( <AnimatePresence mode='wait'>
<NavigationButton {loading ? (
title='Перейти на страницу логина' <AnimateFade key='nav_user_badge_loader'>
icon={<IconLogin size='1.5rem' className='icon-primary' />} <Loader circular size={3} />
onClick={navigateLogin} </AnimateFade>
/> ) : null}
) : null} {!user && !loading ? (
{user ? <NavigationButton icon={<FaCircleUser size='1.5rem' />} onClick={menu.toggle} /> : null} <AnimateFade key='nav_user_badge_login' className='h-full'>
<NavigationButton
title='Перейти на страницу логина'
icon={<IconLogin size='1.5rem' className='icon-primary' />}
onClick={navigateLogin}
/>
</AnimateFade>
) : null}
{user ? (
<AnimateFade key='nav_user_badge_profile' className='h-full'>
<NavigationButton
icon={<FaCircleUser size='1.5rem' className={adminMode && user.is_staff ? 'icon-primary' : ''} />}
onClick={menu.toggle}
/>
</AnimateFade>
) : null}
</AnimatePresence>
<UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} /> <UserDropdown isOpen={!!user && menu.isOpen} hideDropdown={() => menu.hide()} />
</div> </div>
); );

View File

@ -2,6 +2,8 @@
* Module: Internal navigation constants. * Module: Internal navigation constants.
*/ */
import { buildConstants } from '@/utils/constants';
/** /**
* Routes. * Routes.
*/ */
@ -29,6 +31,7 @@ interface SchemaProps {
* Internal navigation URLs. * Internal navigation URLs.
*/ */
export const urls = { export const urls = {
admin: `${buildConstants.backend}/admin`,
home: '/', home: '/',
login: `/${routes.login}`, login: `/${routes.login}`,
login_hint: (userName: string) => `/login?username=${userName}`, login_hint: (userName: string) => `/login?username=${userName}`,

View File

@ -1,5 +1,6 @@
// Search new icons at https://reactsvgicons.com/ // Search new icons at https://reactsvgicons.com/
export { LuLogOut as IconLogout } from 'react-icons/lu';
export { FiSave as IconSave } from 'react-icons/fi'; export { FiSave as IconSave } from 'react-icons/fi';
export { BiCheck as IconAccept } from 'react-icons/bi'; export { BiCheck as IconAccept } from 'react-icons/bi';
export { BiX as IconClose } 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 { BiCog as IconSettings } from 'react-icons/bi';
export { LuUserCircle2 as IconUser } from 'react-icons/lu'; export { LuUserCircle2 as IconUser } from 'react-icons/lu';
export { LuCrown as IconOwner } 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 { LuGlasses as IconReader } from 'react-icons/lu';
export { FiBell as IconFollow } from 'react-icons/fi'; export { FiBell as IconFollow } from 'react-icons/fi';
export { FiBellOff as IconFollowOff } 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 { LuChevronDown as IconDropArrow } from 'react-icons/lu';
export { LuChevronUp as IconDropArrowUp } 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 { LuImage as IconImage } from 'react-icons/lu';
export { TbColumns as IconList } from 'react-icons/tb'; export { TbColumns as IconList } from 'react-icons/tb';
export { TbColumnsOff as IconListOff } from 'react-icons/tb'; export { TbColumnsOff as IconListOff } from 'react-icons/tb';

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { ThreeDots } from 'react-loader-spinner'; import { ThreeCircles, ThreeDots } from 'react-loader-spinner';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
@ -8,13 +8,18 @@ import AnimateFade from '../wrap/AnimateFade';
interface LoaderProps { interface LoaderProps {
size?: number; size?: number;
circular?: boolean;
} }
function Loader({ size = 10 }: LoaderProps) { function Loader({ size = 10, circular }: LoaderProps) {
const { colors } = useConceptOptions(); const { colors } = useConceptOptions();
return ( return (
<AnimateFade noFadeIn className='flex justify-center'> <AnimateFade noFadeIn className='flex justify-center'>
<ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} /> {circular ? (
<ThreeCircles color={colors.bgPrimary} height={size * 10} width={size * 10} />
) : (
<ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} />
)}
</AnimateFade> </AnimateFade>
); );
} }

View File

@ -2,13 +2,6 @@
import { createContext, useCallback, useContext, useLayoutEffect, useState } from 'react'; 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 { import {
type DataCallback, type DataCallback,
getAuth, getAuth,
@ -20,6 +13,13 @@ import {
postSignup, postSignup,
postValidatePasswordToken postValidatePasswordToken
} from '@/app/backendAPI'; } 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'; import { useUsers } from './UsersContext';
@ -53,13 +53,14 @@ interface AuthStateProps {
export const AuthState = ({ children }: AuthStateProps) => { export const AuthState = ({ children }: AuthStateProps) => {
const { users } = useUsers(); const { users } = useUsers();
const [user, setUser] = useState<ICurrentUser | undefined>(undefined); const [user, setUser] = useState<ICurrentUser | undefined>(undefined);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true);
const [error, setError] = useState<ErrorData>(undefined); const [error, setError] = useState<ErrorData>(undefined);
const reload = useCallback( const reload = useCallback(
(callback?: () => void) => { (callback?: () => void) => {
getAuth({ getAuth({
onError: () => setUser(undefined), onError: () => setUser(undefined),
setLoading: setLoading,
onSuccess: currentUser => { onSuccess: currentUser => {
if (currentUser.id) { if (currentUser.id) {
setUser(currentUser); setUser(currentUser);

View File

@ -19,6 +19,9 @@ interface IOptionsContext {
darkMode: boolean; darkMode: boolean;
toggleDarkMode: () => void; toggleDarkMode: () => void;
adminMode: boolean;
toggleAdminMode: () => void;
mathFont: FontStyle; mathFont: FontStyle;
setMathFont: (value: FontStyle) => void; setMathFont: (value: FontStyle) => void;
@ -53,6 +56,7 @@ interface OptionsStateProps {
export const OptionsState = ({ children }: OptionsStateProps) => { export const OptionsState = ({ children }: OptionsStateProps) => {
const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false); const [darkMode, setDarkMode] = useLocalStorage(storage.themeDark, false);
const [adminMode, setAdminMode] = useLocalStorage(storage.themeDark, false);
const [mathFont, setMathFont] = useLocalStorage<FontStyle>(storage.rseditFont, 'math'); const [mathFont, setMathFont] = useLocalStorage<FontStyle>(storage.rseditFont, 'math');
const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true); const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true);
const [noNavigation, setNoNavigation] = useState(false); const [noNavigation, setNoNavigation] = useState(false);
@ -121,6 +125,7 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
<OptionsContext.Provider <OptionsContext.Provider
value={{ value={{
darkMode, darkMode,
adminMode,
colors, colors,
mathFont, mathFont,
setMathFont, setMathFont,
@ -130,6 +135,7 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
showScroll, showScroll,
showHelp, showHelp,
toggleDarkMode: toggleDarkMode, toggleDarkMode: toggleDarkMode,
toggleAdminMode: () => setAdminMode(prev => !prev),
toggleNoNavigation: toggleNoNavigation, toggleNoNavigation: toggleNoNavigation,
setNoFooter, setNoFooter,
setShowScroll, setShowScroll,

View File

@ -14,6 +14,7 @@ import TextURL from '@/components/ui/TextURL';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
import { useConceptOptions } from '@/context/OptionsContext';
import { useRSForm } from '@/context/RSFormContext'; import { useRSForm } from '@/context/RSFormContext';
import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem'; import DlgCloneLibraryItem from '@/dialogs/DlgCloneLibraryItem';
import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate'; import DlgConstituentaTemplate from '@/dialogs/DlgConstituentaTemplate';
@ -120,6 +121,7 @@ export const RSEditState = ({
}: RSEditStateProps) => { }: RSEditStateProps) => {
const router = useConceptNavigation(); const router = useConceptNavigation();
const { user } = useAuth(); const { user } = useAuth();
const { adminMode } = useConceptOptions();
const { mode, setMode } = useAccessMode(); const { mode, setMode } = useAccessMode();
const model = useRSForm(); const model = useRSForm();
@ -152,15 +154,15 @@ export const RSEditState = ({
useLayoutEffect( useLayoutEffect(
() => () =>
setMode(prev => { setMode(prev => {
if (prev === UserAccessMode.ADMIN) { if (user?.is_staff && (prev === UserAccessMode.ADMIN || adminMode)) {
return prev; return UserAccessMode.ADMIN;
} else if (model.isOwned) { } else if (model.isOwned) {
return UserAccessMode.OWNER; return UserAccessMode.OWNER;
} else { } else {
return UserAccessMode.READER; return UserAccessMode.READER;
} }
}), }),
[model.schema, setMode, model.isOwned] [model.schema, setMode, model.isOwned, user, adminMode]
); );
const viewVersion = useCallback( const viewVersion = useCallback(

View File

@ -243,7 +243,7 @@ export const animateFade = {
transition: { transition: {
type: 'tween', type: 'tween',
ease: 'linear', ease: 'linear',
duration: 0.3 duration: 0.4
} }
}, },
hidden: { hidden: {
@ -251,7 +251,7 @@ export const animateFade = {
transition: { transition: {
type: 'tween', type: 'tween',
ease: 'linear', ease: 'linear',
duration: 0.3 duration: 0.4
} }
} }
}, },
@ -260,7 +260,7 @@ export const animateFade = {
transition: { transition: {
type: 'tween', type: 'tween',
ease: 'linear', ease: 'linear',
duration: 0.3 duration: 0.4
} }
} }
}; };