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 { 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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user