mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add frontend admin tools and tune animations
This commit is contained in:
parent
128bdf5ec4
commit
7bc65ad01a
|
@ -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 (
|
||||
<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
|
||||
text={user?.username}
|
||||
title='Профиль пользователя'
|
||||
|
@ -55,10 +60,21 @@ function UserDropdown({ isOpen, hideDropdown }: UserDropdownProps) {
|
|||
title='Отображение иконок подсказок'
|
||||
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
|
||||
text='Выйти...'
|
||||
className='font-semibold'
|
||||
icon={<LuLogOut size='1rem' />}
|
||||
icon={<IconLogout size='1rem' />}
|
||||
onClick={logoutAndRedirect}
|
||||
/>
|
||||
</Dropdown>
|
||||
|
|
|
@ -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 (
|
||||
<div ref={menu.ref} className='h-full'>
|
||||
{!user ? (
|
||||
<div ref={menu.ref} className='h-full w-[4rem] flex items-center justify-center'>
|
||||
<AnimatePresence mode='wait'>
|
||||
{loading ? (
|
||||
<AnimateFade key='nav_user_badge_loader'>
|
||||
<Loader circular size={3} />
|
||||
</AnimateFade>
|
||||
) : null}
|
||||
{!user && !loading ? (
|
||||
<AnimateFade key='nav_user_badge_login' className='h-full'>
|
||||
<NavigationButton
|
||||
title='Перейти на страницу логина'
|
||||
icon={<IconLogin size='1.5rem' className='icon-primary' />}
|
||||
onClick={navigateLogin}
|
||||
/>
|
||||
</AnimateFade>
|
||||
) : null}
|
||||
{user ? <NavigationButton icon={<FaCircleUser size='1.5rem' />} onClick={menu.toggle} /> : 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()} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -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}`,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 (
|
||||
<AnimateFade noFadeIn className='flex justify-center'>
|
||||
{circular ? (
|
||||
<ThreeCircles color={colors.bgPrimary} height={size * 10} width={size * 10} />
|
||||
) : (
|
||||
<ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} />
|
||||
)}
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<ICurrentUser | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<ErrorData>(undefined);
|
||||
|
||||
const reload = useCallback(
|
||||
(callback?: () => void) => {
|
||||
getAuth({
|
||||
onError: () => setUser(undefined),
|
||||
setLoading: setLoading,
|
||||
onSuccess: currentUser => {
|
||||
if (currentUser.id) {
|
||||
setUser(currentUser);
|
||||
|
|
|
@ -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<FontStyle>(storage.rseditFont, 'math');
|
||||
const [showHelp, setShowHelp] = useLocalStorage(storage.optionsHelp, true);
|
||||
const [noNavigation, setNoNavigation] = useState(false);
|
||||
|
@ -121,6 +125,7 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
|
|||
<OptionsContext.Provider
|
||||
value={{
|
||||
darkMode,
|
||||
adminMode,
|
||||
colors,
|
||||
mathFont,
|
||||
setMathFont,
|
||||
|
@ -130,6 +135,7 @@ export const OptionsState = ({ children }: OptionsStateProps) => {
|
|||
showScroll,
|
||||
showHelp,
|
||||
toggleDarkMode: toggleDarkMode,
|
||||
toggleAdminMode: () => setAdminMode(prev => !prev),
|
||||
toggleNoNavigation: toggleNoNavigation,
|
||||
setNoFooter,
|
||||
setShowScroll,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user