mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Refactor UserProfileContext
This commit is contained in:
parent
f03e61e3c1
commit
ae8b4afa88
|
@ -7,7 +7,8 @@ interface RequireAuthProps {
|
|||
}
|
||||
|
||||
function RequireAuth({ children }: RequireAuthProps) {
|
||||
const { user } = useAuth()
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
{user && children}
|
||||
|
|
77
rsconcept/frontend/src/context/UserProfileContext.tsx
Normal file
77
rsconcept/frontend/src/context/UserProfileContext.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { ErrorInfo } from '../components/BackendError';
|
||||
import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI';
|
||||
import { IUserProfile, IUserUpdateData } from '../utils/models';
|
||||
|
||||
interface IUserProfileContextContext {
|
||||
user: IUserProfile | undefined
|
||||
loading: boolean
|
||||
processing: boolean
|
||||
error: ErrorInfo
|
||||
setError: (error: ErrorInfo) => void
|
||||
updateUser: (data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => void
|
||||
}
|
||||
|
||||
const ProfileContext = createContext<IUserProfileContextContext | null>(null);
|
||||
export const useUserProfile = () => {
|
||||
const context = useContext(ProfileContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'useUserProfile has to be used within <UserProfileState.Provider>'
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface UserProfileStateProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const UserProfileState = ({ children }: UserProfileStateProps) => {
|
||||
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
||||
|
||||
const reload = useCallback(
|
||||
() => {
|
||||
setError(undefined);
|
||||
setUser(undefined);
|
||||
getProfile({
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: error => { setError(error); },
|
||||
onSuccess: newData => { setUser(newData); }
|
||||
});
|
||||
}, [setUser]
|
||||
);
|
||||
|
||||
const updateUser = useCallback(
|
||||
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
|
||||
setError(undefined);
|
||||
patchProfile({
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setProcessing,
|
||||
onError: error => { setError(error); },
|
||||
onSuccess: newData => {
|
||||
setUser(newData);
|
||||
if (callback) callback(newData);
|
||||
}
|
||||
});
|
||||
}, [setUser]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
return (
|
||||
<ProfileContext.Provider
|
||||
value={{user, updateUser, error, loading, setError, processing}}
|
||||
>
|
||||
{children}
|
||||
</ProfileContext.Provider>
|
||||
);
|
||||
};
|
|
@ -1,45 +0,0 @@
|
|||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { type ErrorInfo } from '../components/BackendError'
|
||||
import { DataCallback, getProfile, patchProfile } from '../utils/backendAPI'
|
||||
import { type IUserProfile,IUserUpdateData } from '../utils/models'
|
||||
|
||||
export function useUserProfile() {
|
||||
const [user, setUser] = useState<IUserProfile | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<ErrorInfo>(undefined);
|
||||
|
||||
const reload = useCallback(
|
||||
() => {
|
||||
setError(undefined);
|
||||
setUser(undefined);
|
||||
getProfile({
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: error => { setError(error); },
|
||||
onSuccess: newData => { setUser(newData); }
|
||||
});
|
||||
}, [setUser]
|
||||
)
|
||||
const updateUser = useCallback(
|
||||
(data: IUserUpdateData, callback?: DataCallback<IUserProfile>) => {
|
||||
setError(undefined);
|
||||
patchProfile({
|
||||
data: data,
|
||||
showError: true,
|
||||
setLoading: setLoading,
|
||||
onError: error => { setError(error); },
|
||||
onSuccess: newData => {
|
||||
setUser(newData);
|
||||
if (callback) callback(newData);
|
||||
}
|
||||
});
|
||||
}, [setUser]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload])
|
||||
|
||||
return { user, updateUser, error, loading };
|
||||
}
|
|
@ -15,6 +15,7 @@ import { IRSFormCreateData, IRSFormMeta } from '../utils/models';
|
|||
|
||||
function CreateRSFormPage() {
|
||||
const navigate = useNavigate();
|
||||
const { createSchema, error, setError, loading } = useNewRSForm()
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [alias, setAlias] = useState('');
|
||||
|
@ -22,25 +23,24 @@ function CreateRSFormPage() {
|
|||
const [common, setCommon] = useState(false);
|
||||
const [file, setFile] = useState<File | undefined>()
|
||||
|
||||
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
}, [title, alias, setError]);
|
||||
|
||||
function handleFile(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
if (event.target.files && event.target.files.length > 0) {
|
||||
setFile(event.target.files[0]);
|
||||
} else {
|
||||
setFile(undefined)
|
||||
setFile(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const onSuccess = (newSchema: IRSFormMeta) => {
|
||||
function onSuccess(newSchema: IRSFormMeta) {
|
||||
toast.success('Схема успешно создана');
|
||||
navigate(`/rsforms/${newSchema.id}`);
|
||||
}
|
||||
const { createSchema, error, setError, loading } = useNewRSForm()
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined)
|
||||
}, [title, alias, setError]);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (loading) {
|
||||
return;
|
||||
|
@ -54,43 +54,49 @@ function CreateRSFormPage() {
|
|||
fileName: file?.name
|
||||
};
|
||||
createSchema(data, onSuccess);
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<RequireAuth>
|
||||
<Form title='Создание концептуальной схемы' onSubmit={handleSubmit} widthClass='max-w-lg mt-4'>
|
||||
<TextInput id='title' label='Полное название' type='text'
|
||||
required={!file}
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
value={title}
|
||||
onChange={event => { setTitle(event.target.value); }}
|
||||
/>
|
||||
<TextInput id='alias' label='Сокращение' type='text'
|
||||
required={!file}
|
||||
value={alias}
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
widthClass='max-w-sm'
|
||||
onChange={event => { setAlias(event.target.value); }}
|
||||
/>
|
||||
<TextArea id='comment' label='Комментарий'
|
||||
value={comment}
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
onChange={event => { setComment(event.target.value); }}
|
||||
/>
|
||||
<Checkbox id='common' label='Общедоступная схема'
|
||||
value={common}
|
||||
onChange={event => { setCommon(event.target.checked); }}
|
||||
/>
|
||||
<FileInput id='trs' label='Загрузить *.trs'
|
||||
acceptType='.trs'
|
||||
onChange={handleFile}
|
||||
/>
|
||||
<Form title='Создание концептуальной схемы'
|
||||
onSubmit={handleSubmit}
|
||||
widthClass='max-w-lg mt-4'
|
||||
>
|
||||
<TextInput id='title' label='Полное название' type='text'
|
||||
required={!file}
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
value={title}
|
||||
onChange={event => setTitle(event.target.value)}
|
||||
/>
|
||||
<TextInput id='alias' label='Сокращение' type='text'
|
||||
required={!file}
|
||||
value={alias}
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
widthClass='max-w-sm'
|
||||
onChange={event => setAlias(event.target.value)}
|
||||
/>
|
||||
<TextArea id='comment' label='Комментарий'
|
||||
value={comment}
|
||||
placeholder={file && 'Загрузить из файла'}
|
||||
onChange={event => setComment(event.target.value)}
|
||||
/>
|
||||
<Checkbox id='common' label='Общедоступная схема'
|
||||
value={common}
|
||||
onChange={event => setCommon(event.target.checked)}
|
||||
/>
|
||||
<FileInput id='trs' label='Загрузить *.trs'
|
||||
acceptType='.trs'
|
||||
onChange={handleFile}
|
||||
/>
|
||||
|
||||
<div className='flex items-center justify-center py-2 mt-4'>
|
||||
<SubmitButton text='Создать схему' loading={loading} />
|
||||
</div>
|
||||
{ error && <BackendError error={error} />}
|
||||
</Form>
|
||||
<div className='flex items-center justify-center py-2 mt-4'>
|
||||
<SubmitButton
|
||||
text='Создать схему'
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
{ error && <BackendError error={error} />}
|
||||
</Form>
|
||||
</RequireAuth>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,12 +11,12 @@ import { useAuth } from '../context/AuthContext';
|
|||
import { IUserLoginData } from '../utils/models';
|
||||
|
||||
function LoginPage() {
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const { user, login, loading, error, setError } = useAuth()
|
||||
|
||||
const navigate = useNavigate();
|
||||
const search = useLocation().search;
|
||||
const { user, login, loading, error, setError } = useAuth();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const name = new URLSearchParams(search).get('username');
|
||||
|
@ -28,7 +28,7 @@ function LoginPage() {
|
|||
setError(undefined);
|
||||
}, [username, password, setError]);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!loading) {
|
||||
const data: IUserLoginData = {
|
||||
|
@ -37,7 +37,7 @@ function LoginPage() {
|
|||
};
|
||||
login(data, () => { navigate('/library?filter=personal'); });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full py-2'> { user
|
||||
|
|
|
@ -32,13 +32,13 @@ export enum RSTabsList {
|
|||
function RSTabs() {
|
||||
const navigate = useNavigate();
|
||||
const { setActiveID, activeID, error, schema, loading, cstCreate } = useRSForm();
|
||||
|
||||
const [activeTab, setActiveTab] = useLocalStorage('rsform_edit_tab', RSTabsList.CARD);
|
||||
|
||||
const [init, setInit] = useState(false);
|
||||
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
|
||||
const [showClone, setShowClone] = useState(false);
|
||||
|
||||
const [syntaxTree, setSyntaxTree] = useState<SyntaxTree>([]);
|
||||
const [showAST, setShowAST] = useState(false);
|
||||
|
||||
|
@ -46,60 +46,6 @@ function RSTabs() {
|
|||
const [insertWhere, setInsertWhere] = useState<number | undefined>(undefined);
|
||||
const [showCreateCst, setShowCreateCst] = useState(false);
|
||||
|
||||
const handleAddNew = useCallback(
|
||||
(type: CstType, selectedCst?: number) => {
|
||||
if (!schema?.items) {
|
||||
return;
|
||||
}
|
||||
const data: ICstCreateData = {
|
||||
cst_type: type,
|
||||
alias: createAliasFor(type, schema),
|
||||
insert_after: selectedCst ?? insertWhere ?? null
|
||||
}
|
||||
cstCreate(data, newCst => {
|
||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||
navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`);
|
||||
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}, timeout_updateUI);
|
||||
}
|
||||
});
|
||||
}, [schema, cstCreate, insertWhere, navigate, activeTab]);
|
||||
|
||||
const onShowCreateCst = useCallback(
|
||||
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
|
||||
if (skipDialog && type) {
|
||||
handleAddNew(type, selectedID);
|
||||
} else {
|
||||
setDefaultType(type);
|
||||
setInsertWhere(selectedID);
|
||||
setShowCreateCst(true);
|
||||
}
|
||||
}, [handleAddNew]);
|
||||
|
||||
const onShowAST = useCallback(
|
||||
(ast: SyntaxTree) => {
|
||||
setSyntaxTree(ast);
|
||||
setShowAST(true);
|
||||
}, []);
|
||||
|
||||
const onEditCst = (cst: IConstituenta) => {
|
||||
setActiveID(cst.id);
|
||||
setActiveTab(RSTabsList.CST_EDIT)
|
||||
};
|
||||
|
||||
const onSelectTab = (index: number) => {
|
||||
setActiveTab(index);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (schema) {
|
||||
const url = new URL(window.location.href);
|
||||
|
@ -146,6 +92,61 @@ function RSTabs() {
|
|||
}
|
||||
}, [activeTab, activeID, init]);
|
||||
|
||||
function onSelectTab(index: number) {
|
||||
setActiveTab(index);
|
||||
}
|
||||
|
||||
const handleAddNew = useCallback(
|
||||
(type: CstType, selectedCst?: number) => {
|
||||
if (!schema?.items) {
|
||||
return;
|
||||
}
|
||||
const data: ICstCreateData = {
|
||||
cst_type: type,
|
||||
alias: createAliasFor(type, schema),
|
||||
insert_after: selectedCst ?? insertWhere ?? null
|
||||
}
|
||||
cstCreate(data, newCst => {
|
||||
toast.success(`Конституента добавлена: ${newCst.alias}`);
|
||||
navigate(`/rsforms/${schema.id}?tab=${activeTab}&active=${newCst.id}`);
|
||||
if (activeTab === RSTabsList.CST_EDIT || activeTab == RSTabsList.CST_LIST) {
|
||||
setTimeout(() => {
|
||||
const element = document.getElementById(`${prefixes.cst_list}${newCst.alias}`);
|
||||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
}
|
||||
}, timeout_updateUI);
|
||||
}
|
||||
});
|
||||
}, [schema, cstCreate, insertWhere, navigate, activeTab]);
|
||||
|
||||
const onShowCreateCst = useCallback(
|
||||
(selectedID: number | undefined, type: CstType | undefined, skipDialog?: boolean) => {
|
||||
if (skipDialog && type) {
|
||||
handleAddNew(type, selectedID);
|
||||
} else {
|
||||
setDefaultType(type);
|
||||
setInsertWhere(selectedID);
|
||||
setShowCreateCst(true);
|
||||
}
|
||||
}, [handleAddNew]);
|
||||
|
||||
const onShowAST = useCallback(
|
||||
(ast: SyntaxTree) => {
|
||||
setSyntaxTree(ast);
|
||||
setShowAST(true);
|
||||
}, []);
|
||||
|
||||
const onEditCst = useCallback(
|
||||
(cst: IConstituenta) => {
|
||||
setActiveID(cst.id);
|
||||
setActiveTab(RSTabsList.CST_EDIT)
|
||||
}, [setActiveID, setActiveTab]);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{ loading && <Loader /> }
|
||||
|
|
|
@ -12,6 +12,8 @@ import { type IUserSignupData } from '../utils/models';
|
|||
|
||||
function RegisterPage() {
|
||||
const navigate = useNavigate();
|
||||
const { user, signup, loading, error, setError } = useAuth();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
@ -19,13 +21,11 @@ function RegisterPage() {
|
|||
const [firstName, setFirstName] = useState('');
|
||||
const [lastName, setLastName] = useState('');
|
||||
|
||||
const { user, signup, loading, error, setError } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
}, [username, email, password, password2, setError]);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!loading) {
|
||||
const data: IUserSignupData = {
|
||||
|
@ -41,7 +41,7 @@ function RegisterPage() {
|
|||
toast.success(`Пользователь успешно создан: ${createdUser.username}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full py-2'>
|
||||
|
|
|
@ -2,12 +2,12 @@ import { useLayoutEffect, useState } from 'react';
|
|||
import { toast } from 'react-toastify';
|
||||
|
||||
import TextInput from '../../components/Common/TextInput';
|
||||
import { useUserProfile } from '../../hooks/useUserProfile';
|
||||
import { useUserProfile } from '../../context/UserProfileContext';
|
||||
import { IUserUpdateData } from '../../utils/models';
|
||||
|
||||
|
||||
export function UserProfile() {
|
||||
const { updateUser, user} = useUserProfile();
|
||||
const { updateUser, user, processing } = useUserProfile();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
@ -23,7 +23,7 @@ export function UserProfile() {
|
|||
}
|
||||
}, [user]);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const data: IUserUpdateData = {
|
||||
username: username,
|
||||
|
@ -32,18 +32,30 @@ export function UserProfile() {
|
|||
last_name: last_name,
|
||||
};
|
||||
updateUser(data, () => toast.success('Изменения сохранены'));
|
||||
};
|
||||
}
|
||||
|
||||
// console.log(user)
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className='flex-grow max-w-xl px-4 py-2 border min-w-fit'>
|
||||
<div className='flex flex-col items-center justify-center px-2 py-2 border'>
|
||||
<TextInput id='username' label="Логин:" value={username} onChange={event => { setUsername(event.target.value); }}/>
|
||||
<TextInput id='first_name' label="Имя:" value={first_name} onChange={event => { setFirstName(event.target.value); }}/>
|
||||
<TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => { setLastName(event.target.value); }}/>
|
||||
<TextInput id='email' label="Электронная почта:" value={email} onChange={event => { setEmail(event.target.value); }}/>
|
||||
<TextInput id='username'
|
||||
label='Логин:'
|
||||
value={username}
|
||||
onChange={event => setUsername(event.target.value)}
|
||||
/>
|
||||
<TextInput id='first_name'
|
||||
label="Имя:"
|
||||
value={first_name}
|
||||
onChange={event => setFirstName(event.target.value)}
|
||||
/>
|
||||
<TextInput id='last_name' label="Фамилия:" value={last_name} onChange={event => setLastName(event.target.value)}/>
|
||||
<TextInput id='email' label="Электронная почта:" value={email} onChange={event => setEmail(event.target.value)}/>
|
||||
<div className='flex items-center justify-between my-4'>
|
||||
<button className='px-2 py-1 bg-green-500 border' type='submit'>Сохранить</button>
|
||||
<button
|
||||
type='submit'
|
||||
className='px-2 py-1 bg-green-500 border'
|
||||
disabled={processing}>
|
||||
<span>Сохранить</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
18
rsconcept/frontend/src/pages/UserProfilePage/UserTabs.tsx
Normal file
18
rsconcept/frontend/src/pages/UserProfilePage/UserTabs.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import BackendError from '../../components/BackendError';
|
||||
import { Loader } from '../../components/Common/Loader';
|
||||
import { useUserProfile } from '../../context/UserProfileContext';
|
||||
import { UserProfile } from './UserProfile';
|
||||
|
||||
function UserTabs() {
|
||||
const { user, error, loading } = useUserProfile();
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{ loading && <Loader /> }
|
||||
{ error && <BackendError error={error} />}
|
||||
{ user && <UserProfile /> }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserTabs;
|
|
@ -1,17 +1,14 @@
|
|||
import BackendError from '../../components/BackendError';
|
||||
import { Loader } from '../../components/Common/Loader';
|
||||
import { useUserProfile } from '../../hooks/useUserProfile';
|
||||
import { UserProfile } from './UserProfile';
|
||||
import RequireAuth from '../../components/RequireAuth';
|
||||
import { UserProfileState } from '../../context/UserProfileContext';
|
||||
import UserTabs from './UserTabs';
|
||||
|
||||
function UserProfilePage() {
|
||||
const { user, error, loading } = useUserProfile();
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{ loading && <Loader /> }
|
||||
{ error && <BackendError error={error} />}
|
||||
{ user && <UserProfile /> }
|
||||
</div>
|
||||
<RequireAuth>
|
||||
<UserProfileState>
|
||||
<UserTabs />
|
||||
</UserProfileState>
|
||||
</RequireAuth>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user