Improve animations

This commit is contained in:
IRBorisov 2024-01-07 03:29:16 +03:00
parent ead0418564
commit dfdbd4b17c
16 changed files with 168 additions and 127 deletions

View 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;

View File

@ -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;

View 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;

View File

@ -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>
);
}

View File

@ -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;

View File

@ -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);

View File

@ -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>
);
}

View File

@ -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,23 +63,25 @@ function LibraryPage() {
}, []);
return (
<>
{library.loading ? <Loader /> : null}
{library.error ? <InfoError error={library.error} /> : null}
{!library.loading && library.items ? (
<AnimateFadeIn>
<SearchPanel
query={query}
setQuery={setQuery}
strategy={strategy}
total={library.items.length ?? 0}
filtered={items.length}
setFilter={setFilter}
/>
<ViewLibrary resetQuery={resetQuery} items={items} />
</AnimateFadeIn>
) : null}
</>
<DataLoader
id='library-page' //
isLoading={library.loading}
error={library.error}
hasNoData={library.items.length === 0}
>
<SearchPanel
query={query}
setQuery={setQuery}
strategy={strategy}
total={library.items.length ?? 0}
filtered={items.length}
setFilter={setFilter}
/>
<ViewLibrary
resetQuery={resetQuery} //
items={items}
/>
</DataLoader>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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} />
) : (
<>
<StatusIcon status={status} />
<span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span>
</>
)}
<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>
);
}

View File

@ -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[],

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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,37 +28,38 @@ function UserTabs() {
}, [auth, items]);
return (
<>
{loading ? <Loader /> : null}
{error ? <InfoError error={error} /> : null}
{user ? (
<AnimateFadeIn className='flex gap-6 py-2'>
<div>
<Overlay position='top-0 right-0'>
<MiniButton
title='Показать/Скрыть отслеживаемые схемы'
icon={
showSubs ? (
<FiBell size='1.25rem' className='clr-text-primary' />
) : (
<FiBellOff size='1.25rem' className='clr-text-primary' />
)
}
onClick={() => setShowSubs(prev => !prev)}
/>
</Overlay>
<h1 className='mb-4'>Учетные данные пользователя</h1>
<div className='flex py-2'>
<EditorProfile />
<EditorPassword />
</div>
<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
title='Показать/Скрыть отслеживаемые схемы'
icon={
showSubs ? (
<FiBell size='1.25rem' className='clr-text-primary' />
) : (
<FiBellOff size='1.25rem' className='clr-text-primary' />
)
}
onClick={() => setShowSubs(prev => !prev)}
/>
</Overlay>
<h1 className='mb-4'>Учетные данные пользователя</h1>
<div className='flex py-2'>
<EditorProfile />
<EditorPassword />
</div>
<AnimatePresence>
{subscriptions.length > 0 && showSubs ? <ViewSubscriptions items={subscriptions} /> : null}
</AnimatePresence>
</AnimateFadeIn>
) : null}
</>
</div>
<AnimatePresence>
{subscriptions.length > 0 && showSubs ? <ViewSubscriptions items={subscriptions} /> : null}
</AnimatePresence>
</AnimateFade>
</DataLoader>
);
}

View File

@ -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
}
}
};