1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

removed redux completely

This commit is contained in:
Fedor Katurov 2022-01-09 19:03:01 +07:00
parent 26e6d8d41b
commit a4bb07e9cf
323 changed files with 2464 additions and 3348 deletions

View file

@ -1,18 +1,34 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { push } from 'connected-react-router';
import axios, { AxiosError, AxiosResponse } from 'axios';
import { API } from '~/constants/api';
import { store } from '~/redux/store';
import { IApiErrorResult, IResultWithStatus } from '~/redux/types';
export const authMiddleware = r => {
store.dispatch(push('/login'));
return r;
};
import { getMOBXStore } from '~/store';
import { assocPath } from 'ramda';
export const api = axios.create({
baseURL: API.BASE,
});
// Pass token to axios
api.interceptors.request.use(options => {
const token = getMOBXStore().auth.token;
if (!token) {
return options;
}
return assocPath(['headers', 'authorization'], `Bearer ${token}`, options);
});
// Logout on 401
api.interceptors.response.use(undefined, (error: AxiosError<{ error: string }>) => {
if (error.response?.status === HTTP_RESPONSES.UNAUTHORIZED) {
getMOBXStore().auth.logout();
}
error.message = error?.response?.data?.error || error?.response?.statusText || error.message;
throw error;
});
export const HTTP_RESPONSES = {
SUCCESS: 200,
CREATED: 201,
@ -23,32 +39,4 @@ export const HTTP_RESPONSES = {
TOO_MANY_REQUESTS: 429,
};
export const resultMiddleware = <T extends any>({ status, data }: { status; data: T }) => ({
status,
data,
});
export const errorMiddleware = <T extends any = any>(debug): IResultWithStatus<T> =>
debug && debug.response
? {
status: debug.response.status,
data:
(debug.response.data as T & IApiErrorResult) || (debug.response as T & IApiErrorResult),
error: debug?.response?.data?.error || debug?.response?.error || 'Неизвестная ошибка',
}
: {
status: HTTP_RESPONSES.CONNECTION_REFUSED,
data: {} as T & IApiErrorResult & any,
debug,
error: 'Ошибка сети',
};
export const configWithToken = (
access: string,
config: AxiosRequestConfig = {}
): AxiosRequestConfig => ({
...config,
headers: { ...(config.headers || {}), Authorization: `Bearer ${access}` },
});
export const cleanResult = <T extends any>(response: AxiosResponse<T>): T => response?.data;

View file

@ -1,4 +1,4 @@
import { IComment, IFile } from '~/redux/types';
import { IComment, IFile } from '~/types';
import React, { createContext, FC, useContext } from 'react';
export interface CommentProviderProps {

View file

@ -1,5 +1,5 @@
import React, { createContext, FC, useContext } from 'react';
import { FlowDisplay, IFlowNode, INode } from '~/redux/types';
import { FlowDisplay, IFlowNode, INode } from '~/types';
export interface FlowContextProps {
updates: IFlowNode[];

View file

@ -1,6 +1,6 @@
import { ILabNode } from '~/types/lab';
import React, { createContext, FC, useContext } from 'react';
import { IFlowNode, ITag } from '~/redux/types';
import { IFlowNode, ITag } from '~/types';
export interface LabContextProps {
isLoading: boolean;

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { EMPTY_NODE } from '~/constants/node';
import React, { createContext, FC, useContext } from 'react';

View file

@ -1,5 +1,5 @@
import { INodeRelated } from "~/types/node";
import React, { createContext, FC, useContext } from "react";
import { INodeRelated } from '~/types/node';
import React, { createContext, FC, useContext } from 'react';
interface NodeRelatedProviderProps {
related: INodeRelated;

View file

@ -1,5 +1,5 @@
import React, { createContext, FC, useContext } from 'react';
import { ITag } from '~/redux/types';
import { ITag } from '~/types';
export interface TagContextProps {
tags: ITag[];

View file

@ -1,6 +1,6 @@
import React, { createContext, FC, useContext } from 'react';
import { useUploader } from '~/hooks/data/useUploader';
import { IFile } from '~/redux/types';
import { IFile } from '~/types';
import { EMPTY_FILE } from '~/constants/uploads';
export type Uploader = ReturnType<typeof useUploader>;

View file

@ -1,14 +1,15 @@
import React, { createContext, FC, useContext } from 'react';
import { IUser } from '~/redux/auth/types';
import { EMPTY_USER } from '~/redux/auth/constants';
import { useUser } from '~/hooks/user/userUser';
import { IUser } from '~/types/auth';
import { EMPTY_USER } from '~/constants/auth';
import { useUser } from '~/hooks/auth/useUser';
import { observer } from 'mobx-react-lite';
const UserContext = createContext<IUser>(EMPTY_USER);
export const UserContextProvider: FC = ({ children }) => {
const user = useUser();
export const UserContextProvider: FC = observer(({ children }) => {
const { user } = useUser();
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
});
export const useUserContext = () => useContext(UserContext);

View file

@ -1,10 +1,9 @@
import { IFile, ValueOf } from '~/redux/types';
import { IFile, ValueOf } from '~/types';
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
import isAfter from 'date-fns/isAfter';
import differenceInMonths from 'date-fns/differenceInMonths';
import differenceInMinutes from 'date-fns/differenceInMinutes';
import ru from 'date-fns/locale/ru';
import Axios from 'axios';
import { PRESETS } from '~/constants/urls';
import { COMMENT_BLOCK_DETECTORS, COMMENT_BLOCK_TYPES, ICommentBlock } from '~/constants/comment';
import format from 'date-fns/format';
@ -21,18 +20,6 @@ import {
} from '~/utils/formatText';
import { splitTextByYoutube, splitTextOmitEmpty } from '~/utils/splitText';
export const getStyle = (oElm: any, strCssRule: string) => {
if (document.defaultView && document.defaultView.getComputedStyle) {
return document.defaultView.getComputedStyle(oElm, '').getPropertyValue(strCssRule);
}
if (oElm.currentStyle) {
return oElm.currentStyle[strCssRule.replace(/-(\w)/g, (strMatch, p1) => p1.toUpperCase())];
}
return '';
};
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
@ -126,8 +113,6 @@ export const splitCommentByBlocks = (text: string): ICommentBlock[] =>
export const formatCommentText = (author?: string, text?: string): ICommentBlock[] =>
author && text ? splitCommentByBlocks(text) : [];
export const formatCellText = (text: string): string => formatTextParagraphs(text);
export const getPrettyDate = (date?: string): string => {
if (!date) {
return '';
@ -147,30 +132,16 @@ export const getPrettyDate = (date?: string): string => {
});
};
export const getYoutubeTitle = async (id: string) => {
Axios.get(`http://youtube.com/get_video_info?video_id=${id}`).then(console.log);
};
export const getYoutubeThumb = (url: string) => {
const match =
url &&
url.match(
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?/
/http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?[\w?=]*)?/
);
return match && match[1] ? `https://i.ytimg.com/vi/${match[1]}/hqdefault.jpg` : null;
};
export function plural(n: number, one: string, two: string, five: string) {
if (n % 10 === 1 && n % 100 !== 11) {
return `${n} ${one}`;
} else if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20)) {
return `${n} ${two}`;
} else {
return `${n} ${five}`;
}
}
export const stringToColour = (str: string) => {
let hash = 0;
@ -189,26 +160,26 @@ export const stringToColour = (str: string) => {
};
export const darken = (col: string, amt: number) => {
var usePound = false;
let usePound = false;
if (col[0] == '#') {
if (col[0] === '#') {
col = col.slice(1);
usePound = true;
}
var num = parseInt(col, 16);
const num = parseInt(col, 16);
var r = (num >> 16) + amt;
let r = (num >> 16) + amt;
if (r > 255) r = 255;
else if (r < 0) r = 0;
var b = ((num >> 8) & 0x00ff) + amt;
let b = ((num >> 8) & 0x00ff) + amt;
if (b > 255) b = 255;
else if (b < 0) b = 0;
var g = (num & 0x0000ff) + amt;
let g = (num & 0x0000ff) + amt;
if (g > 255) g = 255;
else if (g < 0) g = 0;
@ -217,9 +188,9 @@ export const darken = (col: string, amt: number) => {
};
export const sizeOf = (bytes: number): string => {
if (bytes == 0) {
if (bytes === 0) {
return '0.00 B';
}
var e = Math.floor(Math.log(bytes) / Math.log(1024));
let e = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, e)).toFixed(2) + ' ' + ' KMGTP'.charAt(e) + 'B';
};

View file

@ -1,7 +1,11 @@
import { has, path } from 'ramda';
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
export const getErrorMessage = (error: unknown) => {
export const getErrorMessage = (error: unknown): string | undefined => {
if (error === undefined) {
return undefined;
}
if (typeof error === 'string' && has(error, ERROR_LITERAL)) {
return ERROR_LITERAL[error];
}

View file

@ -1,5 +1,5 @@
import { curry, insert, nth, path, remove } from 'ramda';
import { IComment, ICommentGroup } from '~/redux/types';
import { IComment, ICommentGroup } from '~/types';
import { isAfter, isValid, parseISO } from 'date-fns';
export const moveArrItem = curry((at, to, list) => insert(to, nth(at, list), remove(at, 1, list)));

View file

@ -1,12 +1,13 @@
import marked from "marked";
import { stripHTMLTags } from "~/utils/stripHTMLTags";
import marked from 'marked';
import { stripHTMLTags } from '~/utils/stripHTMLTags';
import { EventMessageType } from '~/constants/events';
/**
* Cleans youtube urls
*/
export const formatTextSanitizeYoutube = (text: string): string =>
text.replace(
/(https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?[\w\-\&\=]+)/gim,
/(https?:\/\/(www\.)?(youtube\.com|youtu\.be)\/(watch)?(\?v=)?[\w\-&=]+)/gim,
'\n$1\n'
);
@ -21,7 +22,7 @@ export const formatTextSanitizeTags = (text: string): string => stripHTMLTags(te
export const formatTextClickableUsernames = (text: string): string =>
text.replace(
/~([\wа-яА-Я-]+)/giu,
'<span class="username" onClick="window.postMessage({ type: \'username\', username: \'$1\'});">~$1</span>'
`<span class="username" onClick="window.postMessage({ type: '${EventMessageType.OpenProfile}', username: '$1'});">~$1</span>`
);
/**
@ -49,16 +50,7 @@ export const formatTextTodos = (text: string): string =>
* Formats !!exclamation messages with green color
*/
export const formatExclamations = (text: string): string =>
text.replace(/(\!\![\s\S]*?(\!\!|\n|$))/gim, '<span class="green">$1$2</span>');
/**
* Formats links
*/
export const formatLinks = (text: string): string =>
text.replace(
/(\b(https?|ftp|file):\/\/([-A-Z0-9+&@#%?=~_|!:,.;]*)([-A-Z0-9+&@#%?\/=~_|!:,.;]*)[-A-Z0-9+&@#\/%=~_|])/gi,
'<a href="$1" target="blank" rel="nofollow">$1</a>'
);
text.replace(/(!![\s\S]*?(!!|\n|$))/gim, '<span class="green">$1$2</span>');
/**
* Replaces -- with dash

View file

@ -1,19 +1,19 @@
import { USER_ROLES } from '~/redux/auth/constants';
import { ICommentGroup, INode } from '~/redux/types';
import { IUser } from '~/redux/auth/types';
import { Role } from '~/constants/auth';
import { ICommentGroup, INode } from '~/types';
import { IUser } from '~/types/auth';
import { path } from 'ramda';
import { NODE_TYPES } from '~/constants/node';
export const canEditNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], node) === path(['id'], user);
path(['role'], user) === Role.Admin || path(['user', 'id'], node) === path(['id'], user);
export const canEditComment = (comment?: Partial<ICommentGroup>, user?: Partial<IUser>): boolean =>
path(['role'], user) === USER_ROLES.ADMIN || path(['user', 'id'], comment) === path(['id'], user);
path(['role'], user) === Role.Admin || path(['user', 'id'], comment) === path(['id'], user);
export const canLikeNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['role'], user) !== USER_ROLES.GUEST;
path(['role'], user) !== Role.Guest;
export const canStarNode = (node?: Partial<INode>, user?: Partial<IUser>): boolean =>
path(['type'], node) === NODE_TYPES.IMAGE &&
path(['is_promoted'], node) === false &&
path(['role'], user) === USER_ROLES.ADMIN;
path(['role'], user) === Role.Admin;

View file

@ -1,9 +1,9 @@
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from "react";
import { IFile } from "~/redux/types";
import { getURL } from "~/utils/dom";
import { path } from "ramda";
import { PlayerState } from "~/constants/player";
import { PlayerProgress } from "~/types/player";
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from 'react';
import { IFile } from '~/types';
import { getURL } from '~/utils/dom';
import { path } from 'ramda';
import { PlayerState } from '~/constants/player';
import { PlayerProgress } from '~/types/player';
interface AudioPlayerProps {
file?: IFile;

View file

@ -0,0 +1,29 @@
import { createContext, FC, useContext } from 'react';
import { useRestorePasswordRedirect } from '~/hooks/auth/useRestorePasswordRedirect';
import { useMessageEventReactions } from '~/hooks/auth/useMessageEventReactions';
import { observer } from 'mobx-react-lite';
import { useAuth } from '~/hooks/auth/useAuth';
import { EMPTY_USER } from '~/constants/auth';
interface AuthProviderContextType extends ReturnType<typeof useAuth> {}
const AuthContext = createContext<AuthProviderContextType>({
user: EMPTY_USER,
isUser: false,
isTester: false,
setIsTester: isTester => isTester,
logout: () => {},
login: async () => EMPTY_USER,
setToken: () => {},
});
export const AuthProvider: FC = observer(({ children }) => {
const value = useAuth();
useMessageEventReactions();
useRestorePasswordRedirect();
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
});
export const useAuthProvider = () => useContext(AuthContext);

View file

@ -1,6 +1,6 @@
import React, { FC } from "react";
import { LabContextProvider } from "~/utils/context/LabContextProvider";
import { useLab } from "~/hooks/lab/useLab";
import React, { FC } from 'react';
import { LabContextProvider } from '~/utils/context/LabContextProvider';
import { useLab } from '~/hooks/lab/useLab';
interface LabProviderProps {}

View file

@ -1,8 +1,8 @@
import React, { createContext, FC, useContext, useEffect } from "react";
import { MetadataStore } from "~/store/metadata/MetadataStore";
import { observer, useLocalObservable } from "mobx-react-lite";
import { apiGetEmbedYoutube } from "~/api/metadata";
import { EmbedMetadata } from "~/types/metadata";
import React, { createContext, FC, useContext, useEffect } from 'react';
import { MetadataStore } from '~/store/metadata/MetadataStore';
import { observer, useLocalObservable } from 'mobx-react-lite';
import { apiGetEmbedYoutube } from '~/api/metadata';
import { EmbedMetadata } from '~/types/metadata';
interface MetadataContextProps {
metadata: Record<string, EmbedMetadata>;

View file

@ -1,8 +1,8 @@
import React, { FC, useEffect } from "react";
import { INode, ITag } from "~/redux/types";
import { NodeRelatedContextProvider } from "~/utils/context/NodeRelatedContextProvider";
import { INodeRelated } from "~/types/node";
import { useGetNodeRelated } from "~/hooks/node/useGetNodeRelated";
import React, { FC, useEffect } from 'react';
import { INode, ITag } from '~/types';
import { NodeRelatedContextProvider } from '~/utils/context/NodeRelatedContextProvider';
import { INodeRelated } from '~/types/node';
import { useGetNodeRelated } from '~/hooks/node/useGetNodeRelated';
interface NodeRelatedProviderProps {
id: INode['id'];

View file

@ -1,11 +1,7 @@
import { createContext, FC, useCallback, useContext } from 'react';
import { ApiUpdateUserRequest, IUser } from '~/redux/auth/types';
import { createContext, FC, useContext } from 'react';
import { IUser } from '~/types/auth';
import { useGetProfile } from '~/hooks/profile/useGetProfile';
import { EMPTY_USER } from '~/redux/auth/constants';
import { usePatchProfile } from '~/hooks/profile/usePatchProfile';
import { useUser } from '~/hooks/user/userUser';
import { useDispatch } from 'react-redux';
import { authSetUser } from '~/redux/auth/actions';
import { EMPTY_USER } from '~/constants/auth';
interface ProfileProviderProps {
username: string;
@ -14,41 +10,18 @@ interface ProfileProviderProps {
interface ProfileContextValue {
profile: IUser;
isLoading: boolean;
updatePhoto: (file: File) => Promise<unknown>;
updateProfile: (user: Partial<ApiUpdateUserRequest['user']>) => Promise<IUser>;
}
const ProfileContext = createContext<ProfileContextValue>({
profile: EMPTY_USER,
isLoading: false,
updatePhoto: async () => {},
updateProfile: async () => EMPTY_USER,
});
export const ProfileProvider: FC<ProfileProviderProps> = ({ children, username }) => {
const dispatch = useDispatch();
const user = useUser();
const { profile, isLoading, update: updateProfileData } = useGetProfile(username);
const update = useCallback(
async (data: Partial<IUser>) => {
if (profile.id === user.id) {
await updateProfileData(data);
}
// TODO: user updateUser from useGetUser or something
dispatch(authSetUser(data));
},
[updateProfileData, dispatch, profile, user]
);
const { updatePhoto, updateProfile } = usePatchProfile(update);
const { profile, isLoading } = useGetProfile(username);
return (
<ProfileContext.Provider value={{ profile, isLoading, updatePhoto, updateProfile }}>
{children}
</ProfileContext.Provider>
<ProfileContext.Provider value={{ profile, isLoading }}>{children}</ProfileContext.Provider>
);
};

View file

@ -1,5 +1,5 @@
import React, { createContext, FC, useContext } from 'react';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useSearch } from '~/hooks/search/useSearch';
export interface SearchContextProps {

View file

@ -1,9 +0,0 @@
// create-index.tsx
import { Action } from "redux";
type Handlers<State, Types extends string, Actions extends Action<Types>> = {
readonly [Type in Types]: (state: State, action: Actions) => State;
};
export const createReducer = (initialState, handlers) => (state = initialState, action) =>
handlers.hasOwnProperty(action.type) ? handlers[action.type](state, action) : state;

View file

@ -3,7 +3,7 @@ import { flatten, isEmpty } from 'ramda';
export const splitTextByYoutube = (strings: string[]): string[] =>
flatten(
strings.map(str =>
str.split(/(https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?[\w\-\&\=]+)/)
str.split(/(https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?[\w\-&=]+)/)
)
);

View file

@ -1,4 +1,4 @@
import { ITag } from '~/redux/types';
import { ITag } from '~/types';
export const separateTags = (tags: Partial<ITag>[]): Partial<ITag>[][] =>
(tags || []).reduce(

View file

@ -1,5 +1,5 @@
import { ERROR_LITERAL, ERRORS } from '~/constants/errors';
import { ValueOf } from '~/redux/types';
import { ValueOf } from '~/types';
export const t = (string: ValueOf<typeof ERRORS>): ValueOf<typeof ERROR_LITERAL> =>
ERROR_LITERAL[string] || string;

View file

@ -1,9 +1,9 @@
import { VALIDATORS } from "~/utils/validators";
import { FILE_MIMES, UploadType } from "~/constants/uploads";
import { isMimeOfImage } from '~/utils/validators';
import { FILE_MIMES, UploadType } from '~/constants/uploads';
/** if file is image, returns data-uri of thumbnail */
export const uploadGetThumb = async file => {
if (!file.type || !VALIDATORS.IS_IMAGE_MIME(file.type)) return '';
if (!file.type || !isMimeOfImage(file.type)) return '';
return new Promise<string>(resolve => {
const reader = new FileReader();

View file

@ -1,7 +1,9 @@
import { EventMessageType } from '~/constants/events';
export const openUserProfile = (username?: string) => {
if (!username) {
return;
}
window.postMessage({ type: 'username', username }, '*');
window.postMessage({ type: EventMessageType.OpenProfile, username }, '*');
};

View file

@ -1,36 +1,3 @@
import isValid from 'date-fns/isValid';
import { IMAGE_MIME_TYPES } from '~/constants/uploads';
const isValidEmail = (email: string): boolean =>
!!email &&
!!String(email) &&
!!String(email).match(
/^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
);
const isLikeEmail = (email: string): boolean =>
!!email && !!String(email) && !!String(email).match(/^([^\@]+)@([^\@]+)\.([^\@]+)$$/);
const isNonEmpty = (value: string): boolean => !!value && value.trim().length > 0;
const isLikePhone = isNonEmpty;
const isAtLeast = (length: number, value: string): boolean =>
!!value && value.trim().length >= length;
const isMimeOfImage = (mime): boolean => !!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0;
const isDate = (val: string): boolean => !!val && isValid(new Date(val));
const isStringDate = (val: string): boolean => !!val && !!val.match(/^[\d]{2,4}\-[\d]{2}-[\d]{2}/);
export const VALIDATORS = {
EMAIL: isValidEmail,
LIKE_PHONE: isLikePhone,
LIKE_EMAIL: isLikeEmail,
NON_EMPTY: isNonEmpty,
AT_LEAST: length => isAtLeast.bind(null, length),
IS_IMAGE_MIME: isMimeOfImage,
IS_DATE: isDate,
IS_STRINGY_DATE: isStringDate,
EVOLVE: (validator: (val: any) => boolean, error: string) => (val: any) =>
!val || !validator(val) ? error : null,
};
export const isMimeOfImage = (mime): boolean => !!mime && IMAGE_MIME_TYPES.indexOf(mime) >= 0;