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 { isResponseHtml } from '@/utils/utils';
import AnimateFade from './AnimateFade';
import PrettyJson from './ui/PrettyJSON'; import PrettyJson from './ui/PrettyJSON';
export type ErrorData = string | Error | AxiosError | undefined; export type ErrorData = string | Error | AxiosError | undefined;
@ -49,9 +50,9 @@ function DescribeError({ error }: { error: ErrorData }) {
function InfoError({ error }: InfoErrorProps) { function InfoError({ error }: InfoErrorProps) {
return ( 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} /> <DescribeError error={error} />
</div> </AnimateFade>
); );
} }

View File

@ -4,15 +4,19 @@ import { ThreeDots } from 'react-loader-spinner';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
import AnimateFade from '../AnimateFade';
interface LoaderProps { interface LoaderProps {
size?: number; size?: number;
} }
export function Loader({ size = 10 }: LoaderProps) { function Loader({ size = 10 }: LoaderProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
return ( return (
<div className='flex justify-center'> <AnimateFade noFadeIn className='flex justify-center'>
<ThreeDots color={colors.bgSelected} height={size * 10} width={size * 10} radius={size} /> <ThreeDots color={colors.bgPrimary} height={size * 10} width={size * 10} radius={size} />
</div> </AnimateFade>
); );
} }
export default Loader;

View File

@ -97,7 +97,7 @@ export const LibraryState = ({ children }: LibraryStateProps) => {
setError(undefined); setError(undefined);
getRSFormDetails(String(templateID), { getRSFormDetails(String(templateID), {
showError: true, showError: true,
setLoading: setLoading, setLoading: setProcessing,
onError: setError, onError: setError,
onSuccess: data => { onSuccess: data => {
const schema = loadRSFormData(data); const schema = loadRSFormData(data);

View File

@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react';
import { BiDownload } from 'react-icons/bi'; import { BiDownload } from 'react-icons/bi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AnimateFadeIn from '@/components/AnimateFadeIn'; import AnimateFade from '@/components/AnimateFade';
import InfoError from '@/components/InfoError'; import InfoError from '@/components/InfoError';
import RequireAuth from '@/components/RequireAuth'; import RequireAuth from '@/components/RequireAuth';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
@ -79,7 +79,7 @@ function CreateRSFormPage() {
} }
return ( return (
<AnimateFadeIn> <AnimateFade>
<RequireAuth> <RequireAuth>
<form className={clsx('px-6 py-3', classnames.flex_col)} onSubmit={handleSubmit}> <form className={clsx('px-6 py-3', classnames.flex_col)} onSubmit={handleSubmit}>
<h1>Создание концептуальной схемы</h1> <h1>Создание концептуальной схемы</h1>
@ -130,7 +130,7 @@ function CreateRSFormPage() {
{error ? <InfoError error={error} /> : null} {error ? <InfoError error={error} /> : null}
</form> </form>
</RequireAuth> </RequireAuth>
</AnimateFadeIn> </AnimateFade>
); );
} }

View File

@ -2,9 +2,7 @@
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import AnimateFadeIn from '@/components/AnimateFadeIn'; import DataLoader from '@/components/DataLoader';
import InfoError from '@/components/InfoError';
import { Loader } from '@/components/ui/Loader';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { useLibrary } from '@/context/LibraryContext'; import { useLibrary } from '@/context/LibraryContext';
import { useConceptNavigation } from '@/context/NavigationContext'; import { useConceptNavigation } from '@/context/NavigationContext';
@ -65,11 +63,12 @@ function LibraryPage() {
}, []); }, []);
return ( return (
<> <DataLoader
{library.loading ? <Loader /> : null} id='library-page' //
{library.error ? <InfoError error={library.error} /> : null} isLoading={library.loading}
{!library.loading && library.items ? ( error={library.error}
<AnimateFadeIn> hasNoData={library.items.length === 0}
>
<SearchPanel <SearchPanel
query={query} query={query}
setQuery={setQuery} setQuery={setQuery}
@ -78,10 +77,11 @@ function LibraryPage() {
filtered={items.length} filtered={items.length}
setFilter={setFilter} setFilter={setFilter}
/> />
<ViewLibrary resetQuery={resetQuery} items={items} /> <ViewLibrary
</AnimateFadeIn> resetQuery={resetQuery} //
) : null} items={items}
</> />
</DataLoader>
); );
} }

View File

@ -4,7 +4,7 @@ import axios from 'axios';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import AnimateFadeIn from '@/components/AnimateFadeIn'; import AnimateFade from '@/components/AnimateFade';
import ExpectedAnonymous from '@/components/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/ExpectedAnonymous';
import InfoError, { ErrorData } from '@/components/InfoError'; import InfoError, { ErrorData } from '@/components/InfoError';
import SubmitButton from '@/components/ui/SubmitButton'; import SubmitButton from '@/components/ui/SubmitButton';
@ -63,7 +63,7 @@ function LoginPage() {
return <ExpectedAnonymous />; return <ExpectedAnonymous />;
} }
return ( return (
<AnimateFadeIn> <AnimateFade>
<form className={clsx('w-[24rem]', 'pt-12 pb-6 px-6', classnames.flex_col)} onSubmit={handleSubmit}> <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' /> <img alt='Концепт Портал' src={resources.logo} className='max-h-[2.5rem] min-w-[2.5rem] mb-3' />
<TextInput <TextInput
@ -97,7 +97,7 @@ function LoginPage() {
</div> </div>
{error ? <ProcessError error={error} /> : null} {error ? <ProcessError error={error} /> : null}
</form> </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 InfoTopic from '@/components/InfoTopic';
import { HelpTopic } from '@/models/miscellaneous'; import { HelpTopic } from '@/models/miscellaneous';
@ -8,9 +8,9 @@ interface ViewTopicProps {
function ViewTopic({ topic }: ViewTopicProps) { function ViewTopic({ topic }: ViewTopicProps) {
return ( return (
<AnimateFadeIn key={topic} className='px-2 py-2 mx-auto'> <AnimateFade key={topic} className='px-2 py-2 mx-auto'>
<InfoTopic topic={topic} /> <InfoTopic topic={topic} />
</AnimateFadeIn> </AnimateFade>
); );
} }

View File

@ -1,9 +1,10 @@
'use client'; 'use client';
import clsx from 'clsx'; import clsx from 'clsx';
import { AnimatePresence } from 'framer-motion';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Loader } from '@/components/ui/Loader'; import Loader from '@/components/ui/Loader';
import { useConceptTheme } from '@/context/ThemeContext'; import { useConceptTheme } from '@/context/ThemeContext';
import { ExpressionStatus } from '@/models/rsform'; import { ExpressionStatus } from '@/models/rsform';
import { type IConstituenta } 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]' data-tooltip-content='Проверить определение [Ctrl + Q]'
onClick={onAnalyze} onClick={onAnalyze}
> >
{processing ? ( <AnimatePresence mode='wait'>
<Loader size={3} /> {processing ? <Loader key='status-loader' size={3} /> : null}
) : ( {!processing ? (
<> <>
<StatusIcon status={status} /> <StatusIcon status={status} />
<span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span> <span className='pb-[0.125rem] font-controls pr-2'>{labelExpressionStatus(status)}</span>
</> </>
)} ) : null}
</AnimatePresence>
</div> </div>
); );
} }

View File

@ -8,9 +8,9 @@ import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs'; import { TabList, TabPanel, Tabs } from 'react-tabs';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AnimateFadeIn from '@/components/AnimateFadeIn'; import AnimateFade from '@/components/AnimateFade';
import InfoError, { ErrorData } from '@/components/InfoError'; 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 TabLabel from '@/components/ui/TabLabel';
import TextURL from '@/components/ui/TextURL'; import TextURL from '@/components/ui/TextURL';
import { useAccessMode } from '@/context/AccessModeContext'; import { useAccessMode } from '@/context/AccessModeContext';
@ -45,19 +45,6 @@ export enum RSTabID {
TERM_GRAPH = 3 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() { function RSTabs() {
const router = useConceptNavigation(); const router = useConceptNavigation();
const query = useQueryStrings(); const query = useQueryStrings();
@ -361,8 +348,6 @@ function RSTabs() {
return ( return (
<> <>
{loading ? <Loader /> : null}
{error ? <ProcessError error={error} /> : null}
<AnimatePresence> <AnimatePresence>
{showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null} {showUpload ? <DlgUploadRSForm hideWindow={() => setShowUpload(false)} /> : null}
{showClone ? <DlgCloneLibraryItem base={schema!} hideWindow={() => setShowClone(false)} /> : null} {showClone ? <DlgCloneLibraryItem base={schema!} hideWindow={() => setShowClone(false)} /> : null}
@ -406,6 +391,8 @@ function RSTabs() {
) : null} ) : null}
</AnimatePresence> </AnimatePresence>
{loading ? <Loader /> : null}
{error ? <ProcessError error={error} /> : null}
{schema && !loading ? ( {schema && !loading ? (
<Tabs <Tabs
selectedIndex={activeTab} selectedIndex={activeTab}
@ -435,7 +422,7 @@ function RSTabs() {
<TabLabel label='Граф термов' /> <TabLabel label='Граф термов' />
</TabList> </TabList>
<AnimateFadeIn> <AnimateFade>
<TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}> <TabPanel forceRender style={{ display: activeTab === RSTabID.CARD ? '' : 'none' }}>
<EditorRSForm <EditorRSForm
isMutable={isMutable} isMutable={isMutable}
@ -481,7 +468,7 @@ function RSTabs() {
onDeleteCst={promptDeleteCst} onDeleteCst={promptDeleteCst}
/> />
</TabPanel> </TabPanel>
</AnimateFadeIn> </AnimateFade>
</Tabs> </Tabs>
) : null} ) : null}
</> </>
@ -491,6 +478,19 @@ function RSTabs() {
export default RSTabs; export default RSTabs;
// ====== Internals ========= // ====== 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( function getNextActiveOnDelete(
activeID: number | undefined, activeID: number | undefined,
items: IConstituenta[], items: IConstituenta[],

View File

@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import { BiInfoCircle } from 'react-icons/bi'; import { BiInfoCircle } from 'react-icons/bi';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import AnimateFadeIn from '@/components/AnimateFadeIn'; import AnimateFade from '@/components/AnimateFade';
import ExpectedAnonymous from '@/components/ExpectedAnonymous'; import ExpectedAnonymous from '@/components/ExpectedAnonymous';
import InfoError from '@/components/InfoError'; import InfoError from '@/components/InfoError';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
@ -68,7 +68,7 @@ function RegisterPage() {
return <ExpectedAnonymous />; return <ExpectedAnonymous />;
} }
return ( return (
<AnimateFadeIn> <AnimateFade>
<form className={clsx('px-6 py-3', classnames.flex_col)} onSubmit={handleSubmit}> <form className={clsx('px-6 py-3', classnames.flex_col)} onSubmit={handleSubmit}>
<h1>Новый пользователь</h1> <h1>Новый пользователь</h1>
<div className='flex gap-12'> <div className='flex gap-12'>
@ -148,7 +148,7 @@ function RegisterPage() {
</div> </div>
{error ? <InfoError error={error} /> : null} {error ? <InfoError error={error} /> : null}
</form> </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 TextURL from '@/components/ui/TextURL';
import { urls } from '@/utils/constants'; import { urls } from '@/utils/constants';
function RestorePasswordPage() { function RestorePasswordPage() {
return ( return (
<AnimateFadeIn className='py-3'> <AnimateFade className='py-3'>
<p>Автоматическое восстановление пароля не доступно.</p> <p>Автоматическое восстановление пароля не доступно.</p>
<p> <p>
Возможно восстановление пароля через обращение на <TextURL href={urls.mail_portal} text='portal@acconcept.ru' /> Возможно восстановление пароля через обращение на <TextURL href={urls.mail_portal} text='portal@acconcept.ru' />
</p> </p>
</AnimateFadeIn> </AnimateFade>
); );
} }

View File

@ -4,9 +4,8 @@ import { AnimatePresence } from 'framer-motion';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { FiBell, FiBellOff } from 'react-icons/fi'; import { FiBell, FiBellOff } from 'react-icons/fi';
import AnimateFadeIn from '@/components/AnimateFadeIn'; import AnimateFade from '@/components/AnimateFade';
import InfoError from '@/components/InfoError'; import DataLoader from '@/components/DataLoader';
import { Loader } from '@/components/ui/Loader';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
@ -29,11 +28,13 @@ function UserTabs() {
}, [auth, items]); }, [auth, items]);
return ( return (
<> <DataLoader
{loading ? <Loader /> : null} id='profile-page' //
{error ? <InfoError error={error} /> : null} isLoading={loading}
{user ? ( error={error}
<AnimateFadeIn className='flex gap-6 py-2'> hasNoData={!user}
>
<AnimateFade className='flex gap-6 py-2'>
<div> <div>
<Overlay position='top-0 right-0'> <Overlay position='top-0 right-0'>
<MiniButton <MiniButton
@ -57,9 +58,8 @@ function UserTabs() {
<AnimatePresence> <AnimatePresence>
{subscriptions.length > 0 && showSubs ? <ViewSubscriptions items={subscriptions} /> : null} {subscriptions.length > 0 && showSubs ? <ViewSubscriptions items={subscriptions} /> : null}
</AnimatePresence> </AnimatePresence>
</AnimateFadeIn> </AnimateFade>
) : null} </DataLoader>
</>
); );
} }

View File

@ -191,7 +191,7 @@ export const animateModal = {
} }
}; };
export const animateFadeIn = { export const animateFade = {
initial: { initial: {
opacity: 0 opacity: 0
}, },
@ -208,7 +208,7 @@ export const animateFadeIn = {
transition: { transition: {
type: 'tween', type: 'tween',
ease: 'linear', ease: 'linear',
duration: 2 duration: 0.3
} }
} }
}; };