mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Improve animations
This commit is contained in:
parent
ead0418564
commit
dfdbd4b17c
25
rsconcept/frontend/src/components/AnimateFade.tsx
Normal file
25
rsconcept/frontend/src/components/AnimateFade.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { motion } from 'framer-motion';
|
||||
|
||||
import { animateFade } from '@/styling/animations';
|
||||
|
||||
import { CProps } from './props';
|
||||
|
||||
interface AnimateFadeProps extends CProps.AnimatedDiv {
|
||||
noFadeIn?: boolean;
|
||||
noFadeOut?: boolean;
|
||||
}
|
||||
|
||||
function AnimateFade({ noFadeIn, noFadeOut, children, ...restProps }: AnimateFadeProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ ...(!noFadeIn ? animateFade.initial : {}) }}
|
||||
animate={{ ...animateFade.animate }}
|
||||
exit={{ ...(!noFadeOut ? animateFade.exit : {}) }}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnimateFade;
|
|
@ -1,22 +0,0 @@
|
|||
import { motion } from 'framer-motion';
|
||||
|
||||
import { animateFadeIn } from '@/styling/animations';
|
||||
|
||||
import { CProps } from './props';
|
||||
|
||||
interface AnimateFadeInProps extends CProps.AnimatedDiv {}
|
||||
|
||||
function AnimateFadeIn({ children, ...restProps }: AnimateFadeInProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ ...animateFadeIn.initial }}
|
||||
animate={{ ...animateFadeIn.animate }}
|
||||
exit={{ ...animateFadeIn.exit }}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnimateFadeIn;
|
31
rsconcept/frontend/src/components/DataLoader.tsx
Normal file
31
rsconcept/frontend/src/components/DataLoader.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
import AnimateFade from './AnimateFade';
|
||||
import InfoError, { ErrorData } from './InfoError';
|
||||
import Loader from './ui/Loader';
|
||||
|
||||
interface DataLoaderProps {
|
||||
id: string;
|
||||
|
||||
isLoading: boolean;
|
||||
error?: ErrorData;
|
||||
hasNoData?: boolean;
|
||||
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function DataLoader({ id, isLoading, hasNoData, error, children }: DataLoaderProps) {
|
||||
return (
|
||||
<AnimatePresence mode='wait'>
|
||||
{isLoading ? <Loader key={`${id}-loader`} /> : null}
|
||||
{error ? <InfoError key={`${id}-error`} error={error} /> : null}
|
||||
{!isLoading && !error && !hasNoData ? (
|
||||
<AnimateFade id={id} key={`${id}-data`}>
|
||||
{children}
|
||||
</AnimateFade>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataLoader;
|
|
@ -2,6 +2,7 @@ import axios, { type AxiosError } from 'axios';
|
|||
|
||||
import { isResponseHtml } from '@/utils/utils';
|
||||
|
||||
import AnimateFade from './AnimateFade';
|
||||
import PrettyJson from './ui/PrettyJSON';
|
||||
|
||||
export type ErrorData = string | Error | AxiosError | undefined;
|
||||
|
@ -49,9 +50,9 @@ function DescribeError({ error }: { error: ErrorData }) {
|
|||
|
||||
function InfoError({ error }: InfoErrorProps) {
|
||||
return (
|
||||
<div className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'>
|
||||
<AnimateFade className='px-3 py-2 min-w-[15rem] text-sm font-semibold select-text clr-text-warning'>
|
||||
<DescribeError error={error} />
|
||||
</div>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,15 +4,19 @@ import { ThreeDots } from 'react-loader-spinner';
|
|||
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
|
||||
import AnimateFade from '../AnimateFade';
|
||||
|
||||
interface LoaderProps {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export function Loader({ size = 10 }: LoaderProps) {
|
||||
function Loader({ size = 10 }: LoaderProps) {
|
||||
const { colors } = useConceptTheme();
|
||||
return (
|
||||
<div className='flex justify-center'>
|
||||
<ThreeDots color={colors.bgSelected} height={size * 10} width={size * 10} radius={size} />
|
||||
</div>
|
||||
<AnimateFade noFadeIn className='flex justify-center'>
|
||||
<ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} />
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
export default Loader;
|
||||
|
|
|
@ -97,7 +97,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
|
|||
setError(undefined);
|
||||
getRSFormDetails(String(templateID), {
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
setLoading: setProcessing,
|
||||
onError: setError,
|
||||
onSuccess: data => {
|
||||
const schema = loadRSFormData(data);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||
import { BiDownload } from 'react-icons/bi';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import InfoError from '@/components/InfoError';
|
||||
import RequireAuth from '@/components/RequireAuth';
|
||||
import Button from '@/components/ui/Button';
|
||||
|
@ -79,7 +79,7 @@ function CreateRSFormPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<AnimateFadeIn>
|
||||
<AnimateFade>
|
||||
<RequireAuth>
|
||||
<form className={clsx('px-6 py-3', classnames.flex_col)} onSubmit={handleSubmit}>
|
||||
<h1>Создание концептуальной схемы</h1>
|
||||
|
@ -130,7 +130,7 @@ function CreateRSFormPage() {
|
|||
{error ? <InfoError error={error} /> : null}
|
||||
</form>
|
||||
</RequireAuth>
|
||||
</AnimateFadeIn>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
|
||||
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import InfoError from '@/components/InfoError';
|
||||
import { Loader } from '@/components/ui/Loader';
|
||||
import DataLoader from '@/components/DataLoader';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLibrary } from '@/context/LibraryContext';
|
||||
import { useConceptNavigation } from '@/context/NavigationContext';
|
||||
|
@ -65,11 +63,12 @@ function LibraryPage() {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{library.loading ? <Loader /> : null}
|
||||
{library.error ? <InfoError error={library.error} /> : null}
|
||||
{!library.loading && library.items ? (
|
||||
<AnimateFadeIn>
|
||||
<DataLoader
|
||||
id='library-page' //
|
||||
isLoading={library.loading}
|
||||
error={library.error}
|
||||
hasNoData={library.items.length === 0}
|
||||
>
|
||||
<SearchPanel
|
||||
query={query}
|
||||
setQuery={setQuery}
|
||||
|
@ -78,10 +77,11 @@ function LibraryPage() {
|
|||
filtered={items.length}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
<ViewLibrary resetQuery={resetQuery} items={items} />
|
||||
</AnimateFadeIn>
|
||||
) : null}
|
||||
</>
|
||||
<ViewLibrary
|
||||
resetQuery={resetQuery} //
|
||||
items={items}
|
||||
/>
|
||||
</DataLoader>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import axios from 'axios';
|
|||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
||||
import InfoError, { ErrorData } from '@/components/InfoError';
|
||||
import SubmitButton from '@/components/ui/SubmitButton';
|
||||
|
@ -63,7 +63,7 @@ function LoginPage() {
|
|||
return <ExpectedAnonymous />;
|
||||
}
|
||||
return (
|
||||
<AnimateFadeIn>
|
||||
<AnimateFade>
|
||||
<form className={clsx('w-[24rem]', 'pt-12 pb-6 px-6', classnames.flex_col)} onSubmit={handleSubmit}>
|
||||
<img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
|
||||
<TextInput
|
||||
|
@ -97,7 +97,7 @@ function LoginPage() {
|
|||
</div>
|
||||
{error ? <ProcessError error={error} /> : null}
|
||||
</form>
|
||||
</AnimateFadeIn>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import InfoTopic from '@/components/InfoTopic';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
|
||||
|
@ -8,9 +8,9 @@ interface ViewTopicProps {
|
|||
|
||||
function ViewTopic({ topic }: ViewTopicProps) {
|
||||
return (
|
||||
<AnimateFadeIn key={topic} className='px-2 py-2 mx-auto'>
|
||||
<AnimateFade key={topic} className='px-2 py-2 mx-auto'>
|
||||
<InfoTopic topic={topic} />
|
||||
</AnimateFadeIn>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Loader } from '@/components/ui/Loader';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import { useConceptTheme } from '@/context/ThemeContext';
|
||||
import { ExpressionStatus } from '@/models/rsform';
|
||||
import { type IConstituenta } from '@/models/rsform';
|
||||
|
@ -51,14 +52,15 @@ function StatusBar({ isModified, processing, constituenta, parseData, onAnalyze
|
|||
data-tooltip-content='Проверить определение [Ctrl + Q]'
|
||||
onClick={onAnalyze}
|
||||
>
|
||||
{processing ? (
|
||||
<Loader size={3} />
|
||||
) : (
|
||||
<AnimatePresence mode='wait'>
|
||||
{processing ? <Loader key='status-loader' size={3} /> : null}
|
||||
{!processing ? (
|
||||
<>
|
||||
<StatusIcon status={status} />
|
||||
<span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
|
|||
import { TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import InfoError, { ErrorData } from '@/components/InfoError';
|
||||
import { Loader } from '@/components/ui/Loader';
|
||||
import Loader from '@/components/ui/Loader';
|
||||
import TabLabel from '@/components/ui/TabLabel';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import { useAccessMode } from '@/context/AccessModeContext';
|
||||
|
@ -45,19 +45,6 @@ export enum RSTabID {
|
|||
TERM_GRAPH = 3
|
||||
}
|
||||
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return (
|
||||
<div className='p-2 text-center'>
|
||||
<p>Схема с указанным идентификатором отсутствует на портале.</p>
|
||||
<TextURL text='Перейти в Библиотеку' href='/library' />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
}
|
||||
|
||||
function RSTabs() {
|
||||
const router = useConceptNavigation();
|
||||
const query = useQueryStrings();
|
||||
|
@ -361,8 +348,6 @@ function RSTabs() {
|
|||
|
||||
return (
|
||||
<>
|
||||
{loading ? <Loader /> : null}
|
||||
{error ? <ProcessError error={error} /> : null}
|
||||
<AnimatePresence>
|
||||
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
|
||||
{showClone ? <DlgCloneLibraryItem base={schema!} hideWindow={() => setShowClone(false)} /> : null}
|
||||
|
@ -406,6 +391,8 @@ function RSTabs() {
|
|||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
{loading ? <Loader /> : null}
|
||||
{error ? <ProcessError error={error} /> : null}
|
||||
{schema && !loading ? (
|
||||
<Tabs
|
||||
selectedIndex={activeTab}
|
||||
|
@ -435,7 +422,7 @@ function RSTabs() {
|
|||
<TabLabel label='Граф термов' />
|
||||
</TabList>
|
||||
|
||||
<AnimateFadeIn>
|
||||
<AnimateFade>
|
||||
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}>
|
||||
<EditorRSForm
|
||||
isMutable={isMutable}
|
||||
|
@ -481,7 +468,7 @@ function RSTabs() {
|
|||
onDeleteCst={promptDeleteCst}
|
||||
/>
|
||||
</TabPanel>
|
||||
</AnimateFadeIn>
|
||||
</AnimateFade>
|
||||
</Tabs>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -491,6 +478,19 @@ function RSTabs() {
|
|||
export default RSTabs;
|
||||
|
||||
// ====== Internals =========
|
||||
function ProcessError({ error }: { error: ErrorData }): React.ReactElement {
|
||||
if (axios.isAxiosError(error) && error.response && error.response.status === 404) {
|
||||
return (
|
||||
<div className='p-2 text-center'>
|
||||
<p>Схема с указанным идентификатором отсутствует на портале.</p>
|
||||
<TextURL text='Перейти в Библиотеку' href='/library' />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <InfoError error={error} />;
|
||||
}
|
||||
}
|
||||
|
||||
function getNextActiveOnDelete(
|
||||
activeID: number | undefined,
|
||||
items: IConstituenta[],
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
|
|||
import { BiInfoCircle } from 'react-icons/bi';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import ExpectedAnonymous from '@/components/ExpectedAnonymous';
|
||||
import InfoError from '@/components/InfoError';
|
||||
import Button from '@/components/ui/Button';
|
||||
|
@ -68,7 +68,7 @@ function RegisterPage() {
|
|||
return <ExpectedAnonymous />;
|
||||
}
|
||||
return (
|
||||
<AnimateFadeIn>
|
||||
<AnimateFade>
|
||||
<form className={clsx('px-6 py-3', classnames.flex_col)} onSubmit={handleSubmit}>
|
||||
<h1>Новый пользователь</h1>
|
||||
<div className='flex gap-12'>
|
||||
|
@ -148,7 +148,7 @@ function RegisterPage() {
|
|||
</div>
|
||||
{error ? <InfoError error={error} /> : null}
|
||||
</form>
|
||||
</AnimateFadeIn>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import TextURL from '@/components/ui/TextURL';
|
||||
import { urls } from '@/utils/constants';
|
||||
|
||||
function RestorePasswordPage() {
|
||||
return (
|
||||
<AnimateFadeIn className='py-3'>
|
||||
<AnimateFade className='py-3'>
|
||||
<p>Автоматическое восстановление пароля не доступно.</p>
|
||||
<p>
|
||||
Возможно восстановление пароля через обращение на <TextURL href={urls.mail_portal} text='portal@acconcept.ru' />
|
||||
</p>
|
||||
</AnimateFadeIn>
|
||||
</AnimateFade>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,8 @@ import { AnimatePresence } from 'framer-motion';
|
|||
import { useMemo, useState } from 'react';
|
||||
import { FiBell, FiBellOff } from 'react-icons/fi';
|
||||
|
||||
import AnimateFadeIn from '@/components/AnimateFadeIn';
|
||||
import InfoError from '@/components/InfoError';
|
||||
import { Loader } from '@/components/ui/Loader';
|
||||
import AnimateFade from '@/components/AnimateFade';
|
||||
import DataLoader from '@/components/DataLoader';
|
||||
import MiniButton from '@/components/ui/MiniButton';
|
||||
import Overlay from '@/components/ui/Overlay';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
@ -29,11 +28,13 @@ function UserTabs() {
|
|||
}, [auth, items]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? <Loader /> : null}
|
||||
{error ? <InfoError error={error} /> : null}
|
||||
{user ? (
|
||||
<AnimateFadeIn className='flex gap-6 py-2'>
|
||||
<DataLoader
|
||||
id='profile-page' //
|
||||
isLoading={loading}
|
||||
error={error}
|
||||
hasNoData={!user}
|
||||
>
|
||||
<AnimateFade className='flex gap-6 py-2'>
|
||||
<div>
|
||||
<Overlay position='top-0 right-0'>
|
||||
<MiniButton
|
||||
|
@ -57,9 +58,8 @@ function UserTabs() {
|
|||
<AnimatePresence>
|
||||
{subscriptions.length > 0 && showSubs ? <ViewSubscriptions items={subscriptions} /> : null}
|
||||
</AnimatePresence>
|
||||
</AnimateFadeIn>
|
||||
) : null}
|
||||
</>
|
||||
</AnimateFade>
|
||||
</DataLoader>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -191,7 +191,7 @@ export const animateModal = {
|
|||
}
|
||||
};
|
||||
|
||||
export const animateFadeIn = {
|
||||
export const animateFade = {
|
||||
initial: {
|
||||
opacity: 0
|
||||
},
|
||||
|
@ -208,7 +208,7 @@ export const animateFadeIn = {
|
|||
transition: {
|
||||
type: 'tween',
|
||||
ease: 'linear',
|
||||
duration: 2
|
||||
duration: 0.3
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user