REPOSITORY / Unmei/Frontend
Comparing changes
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc496b7a4 |
+19
-18
@@ -1,37 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "unmei-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.34",
|
"@fortawesome/fontawesome-svg-core": "1.2.35",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.15.2",
|
"@fortawesome/free-brands-svg-icons": "5.15.3",
|
||||||
"@fortawesome/free-solid-svg-icons": "5.15.2",
|
"@fortawesome/free-solid-svg-icons": "5.15.3",
|
||||||
"@fortawesome/react-fontawesome": "0.1.14",
|
"@fortawesome/react-fontawesome": "0.1.14",
|
||||||
"@sentry/react": "6.2.0",
|
"@sentry/react": "6.3.1",
|
||||||
"@sentry/tracing": "6.2.0",
|
"@sentry/tracing": "6.3.1",
|
||||||
"bbcode-to-react": "0.2.9",
|
"bbcode-to-react": "0.2.9",
|
||||||
"eruda": "2.4.1",
|
"eruda": "2.4.1",
|
||||||
"eruda-dom": "2.0.0",
|
"eruda-dom": "2.0.0",
|
||||||
"eruda-features": "2.0.0",
|
"eruda-features": "2.0.0",
|
||||||
"eruda-memory": "2.0.0",
|
"eruda-memory": "2.0.0",
|
||||||
"react": "17.0.1",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.2",
|
||||||
"react-loader-spinner": "4.0.0",
|
"react-loader-spinner": "4.0.0",
|
||||||
"react-redux": "7.2.2",
|
"react-redux": "7.2.3",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
"react-snowfall": "1.0.2",
|
"react-snowfall": "1.1.0",
|
||||||
|
"react-helmet": "6.1.0",
|
||||||
"redux": "4.0.5"
|
"redux": "4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "14.14.31",
|
"@types/node": "14.14.41",
|
||||||
"@types/react": "17.0.2",
|
"@types/react": "17.0.3",
|
||||||
"@types/react-dom": "17.0.1",
|
"@types/react-dom": "17.0.3",
|
||||||
"@types/react-redux": "7.1.16",
|
"@types/react-redux": "7.1.16",
|
||||||
"@types/react-router-dom": "5.1.7",
|
"@types/react-router-dom": "5.1.7",
|
||||||
"@types/react-transition-group": "4.4.1",
|
"@types/react-helmet": "6.1.1",
|
||||||
"react-scripts": "4.0.2",
|
"react-scripts": "4.0.3",
|
||||||
"redux-devtools-extension": "2.13.8",
|
"redux-devtools-extension": "2.13.9",
|
||||||
"sass": "1.32.8",
|
"sass": "1.32.11",
|
||||||
"typescript": "4.1.5"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
+19
-1
@@ -9,14 +9,32 @@
|
|||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-title" content="Unmei">
|
<meta name="apple-mobile-web-app-title" content="Unmei">
|
||||||
|
<link rel="apple-touch-icon" href="/favicon.png">
|
||||||
|
|
||||||
<meta name="application-name" content="Unmei">
|
<meta name="application-name" content="Unmei">
|
||||||
<meta name="theme-color" content="#0b0b0b">
|
<meta name="theme-color" content="#0b0b0b">
|
||||||
|
|
||||||
<link sizes="512x512" href="/favicon.png">
|
<link sizes="512x512" href="/favicon.png">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||||
|
|
||||||
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||||
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XZ24R3MWFX"></script>
|
||||||
|
<script>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag() { dataLayer.push(arguments); }
|
||||||
|
gtag('js', new Date());
|
||||||
|
|
||||||
|
gtag('config', 'G-XZ24R3MWFX');
|
||||||
|
window.gtag = gtag;
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>
|
||||||
|
Unmei can't work without JavaScript!<br>
|
||||||
|
Unmei не может работать без JavaScript!
|
||||||
|
</noscript>
|
||||||
<main id="root"></main>
|
<main id="root"></main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
"src": "favicon.png",
|
"src": "favicon.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
|
}, {
|
||||||
|
"src": "favicon.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
}],
|
}],
|
||||||
"display": "fullscreen",
|
"display": "fullscreen",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
|||||||
+3
-1
@@ -1,2 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /admin
|
Disallow: /kawaii__neko
|
||||||
|
|
||||||
|
Sitemap: sitemap.xml
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 435 KiB |
Regular → Executable
+3
-3
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
$mainColor: #efefef
|
$mainColor: #efefef
|
||||||
$secondaryColor: #aaaaaa
|
$secondaryColor: #aaaaaa
|
||||||
$linkHoverColor: #efefef
|
$linkHoverColor: #cccccc
|
||||||
|
|
||||||
// BLACK
|
// BLACK
|
||||||
$primaryBackgroundColor: #121212
|
$primaryBackgroundColor: #121212
|
||||||
@@ -52,8 +52,8 @@ $navbarBackgroundColorLight: #d0d0d0
|
|||||||
$errorBackgroundColor: #993333
|
$errorBackgroundColor: #993333
|
||||||
$successBackgroundColor: #339933
|
$successBackgroundColor: #339933
|
||||||
|
|
||||||
$primaryPadding: .5rem
|
$primaryPadding: 8px
|
||||||
$blockPadding: .2rem
|
$blockPadding: 4px
|
||||||
|
|
||||||
$borderRadius: 4px
|
$borderRadius: 4px
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
+19
-17
@@ -1,11 +1,5 @@
|
|||||||
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.space/v1';
|
const baseUrl = process.env.NODE_ENV === "development" ? 'http://localhost:8080/v1' : 'https://api.unmei.space/v1';
|
||||||
|
|
||||||
// function loadRequestFromCache<T>(url: string): T | null {
|
|
||||||
// let cached = sessionStorage.getItem(url);
|
|
||||||
// if(!cached) return null;
|
|
||||||
// return JSON.parse(cached) as T;
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function request<T>(url: string, method: string, body?: string | FormData): Promise<T> {
|
async function request<T>(url: string, method: string, body?: string | FormData): Promise<T> {
|
||||||
let bUrl = baseUrl;
|
let bUrl = baseUrl;
|
||||||
if(baseUrl.endsWith('/'))
|
if(baseUrl.endsWith('/'))
|
||||||
@@ -13,11 +7,10 @@ async function request<T>(url: string, method: string, body?: string | FormData)
|
|||||||
if(url.startsWith('/'))
|
if(url.startsWith('/'))
|
||||||
url = url.slice(1, baseUrl.length);
|
url = url.slice(1, baseUrl.length);
|
||||||
|
|
||||||
// const cached = method.toLowerCase() === 'get' ? loadRequestFromCache<T>(url) : null;
|
// Caching
|
||||||
// if(cached) {
|
const cached = sessionStorage.getItem(url);
|
||||||
// console.debug(`${baseUrl}/${url} loaded from session cache!`);
|
if(cached && method.toLowerCase() === 'get')
|
||||||
// return Promise.resolve(cached);
|
return Promise.resolve(JSON.parse(cached));
|
||||||
// }
|
|
||||||
|
|
||||||
const res = await fetch(`${bUrl}/${url}`, {
|
const res = await fetch(`${bUrl}/${url}`, {
|
||||||
method, body,
|
method, body,
|
||||||
@@ -27,7 +20,7 @@ async function request<T>(url: string, method: string, body?: string | FormData)
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let json: ApiResponse<T>;
|
let json: Unmei.ApiResponse<T>;
|
||||||
try {
|
try {
|
||||||
json = await res.json();
|
json = await res.json();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -40,6 +33,8 @@ async function request<T>(url: string, method: string, body?: string | FormData)
|
|||||||
if(json?.error) return Promise.reject(json.error_data);
|
if(json?.error) return Promise.reject(json.error_data);
|
||||||
if(!res.ok) return Promise.reject(res.statusText);
|
if(!res.ok) return Promise.reject(res.statusText);
|
||||||
if(!json.data) return Promise.reject(null);
|
if(!json.data) return Promise.reject(null);
|
||||||
|
|
||||||
|
// Caching
|
||||||
sessionStorage.setItem(url, JSON.stringify(json.data));
|
sessionStorage.setItem(url, JSON.stringify(json.data));
|
||||||
return Promise.resolve(json.data);
|
return Promise.resolve(json.data);
|
||||||
}
|
}
|
||||||
@@ -61,16 +56,23 @@ const TranslatePlatform: { [id: string]: string } = {
|
|||||||
win: 'Windows',
|
win: 'Windows',
|
||||||
mac: 'macOS',
|
mac: 'macOS',
|
||||||
linux: 'Linux',
|
linux: 'Linux',
|
||||||
ios: 'iOS',
|
ios: 'IOS',
|
||||||
android: 'Andriod'
|
android: 'Andriod'
|
||||||
};
|
};
|
||||||
|
|
||||||
const TranslateExitStatus: { [id: string]: string } = {
|
const TranslateExitStatus: { [id: string]: string } = {
|
||||||
came_out: 'Вышла'
|
in_developing: 'В разработке',
|
||||||
|
came_out: 'Вышло',
|
||||||
|
canceled: 'Отменено',
|
||||||
|
announcement: 'Анонс',
|
||||||
|
postponed: 'Перенесено'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getVersion = () => get<VersionResponse>('version');
|
export const getVersion = () => get<Unmei.VersionResponse>('version').then(r => {
|
||||||
export const version = '1.2.2';
|
r.build = parseInt(r.build.toString());
|
||||||
export const build = 121;
|
return r;
|
||||||
|
});
|
||||||
|
export const version = '1.2';
|
||||||
|
export const build = 123;
|
||||||
|
|
||||||
export { get, post, put, del, TranslateStatus, TranslatePlatform, TranslateExitStatus };
|
export { get, post, put, del, TranslateStatus, TranslatePlatform, TranslateExitStatus };
|
||||||
|
|||||||
Regular → Executable
+3
-2
@@ -1,13 +1,14 @@
|
|||||||
import { post } from "./api";
|
import { post } from "./api";
|
||||||
|
|
||||||
export const restore = (token: string, new_password: string) => post<string>('auth/restore', { token, new_password });
|
export const restore = (token: string, new_password: string) => post<string>('auth/restore', { token, new_password });
|
||||||
export const registerUser = (login: string, password: string, email: string, recaptcha: string) => post<UserType>('auth/register', {
|
export const registerUser = (login: string, password: string, email: string, recaptcha: string) => post<Unmei.UserType>('auth/register', {
|
||||||
login,
|
login,
|
||||||
password,
|
password,
|
||||||
email,
|
email,
|
||||||
recaptcha
|
recaptcha
|
||||||
});
|
});
|
||||||
export const login = (login: string, password: string, recaptcha: string) => post<UserType>('auth/login', {
|
export const registerUserVk = (code: string) => post('/auth/register/vk', { code });
|
||||||
|
export const login = (login: string, password: string, recaptcha: string) => post<Unmei.UserType>('auth/login', {
|
||||||
login,
|
login,
|
||||||
password,
|
password,
|
||||||
recaptcha
|
recaptcha
|
||||||
|
|||||||
Regular → Executable
+9
-3
@@ -1,4 +1,10 @@
|
|||||||
import { get } from "./api";
|
import { del, get, post, put } from "./api";
|
||||||
|
|
||||||
export const fetchChar = (id: number) => get<CharacterType>(`characters/${id}`);
|
export const fetchChars = () => get<Unmei.CharacterType[]>('characters');
|
||||||
export const fetchCharNovels = (id: number) => get<NovelType[]>(`characters/${id}/novels`);
|
export const createChar = (char: Unmei.CharacterType) => post<Unmei.CharacterType>('chars', char);
|
||||||
|
|
||||||
|
export const fetchChar = (id: number) => get<Unmei.CharacterType>(`characters/${id}`);
|
||||||
|
export const deleteChar = (id: number) => del(`characters/${id}`);
|
||||||
|
export const updateChar = (char: Unmei.CharacterType) => put<Unmei.CharacterType>(`characters/${char.id}`, char);
|
||||||
|
|
||||||
|
export const fetchCharNovels = (id: number) => get<Unmei.NovelType[]>(`characters/${id}/novels`);
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { get } from "./api";
|
import { get } from "./api";
|
||||||
|
|
||||||
export const getClubs = () => get<ClubType[]>('/clubs');
|
export const getClubs = () => get<Unmei.ClubType[]>('/clubs');
|
||||||
export const getClub = (id: number) => get<ClubType>(`/clubs/${id}`);
|
export const getClub = (id: number) => get<Unmei.ClubType>(`/clubs/${id}`);
|
||||||
|
|||||||
Regular → Executable
@@ -0,0 +1,3 @@
|
|||||||
|
import { get } from "./api";
|
||||||
|
|
||||||
|
export const fetchGenres = () => get<Unmei.GenreType[]>('genres');
|
||||||
Regular → Executable
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
import { del, get, post, put } from './api';
|
import { del, get, put, post } from './api';
|
||||||
|
|
||||||
export const fetchNews = () => get<PostType[]>('news');
|
export const fetchNews = () => get<Unmei.PostType[]>('news');
|
||||||
export const createPost = (p: PostType) => post<PostType>('news', p);
|
export const createPost = (p: Unmei.PostType) => post<Unmei.PostType>('news', p);
|
||||||
|
|
||||||
export const fetchPost = (id: number) => get<PostType>(`news/${id}`);
|
export const fetchPost = (id: number) => get<Unmei.PostType>(`news/${id}`);
|
||||||
export const updatePost = (post: PostType) => put<PostType>(`news/${post.id}`, post);
|
export const updatePost = (post: Unmei.PostType) => put<Unmei.PostType>(`news/${post.id}`, post);
|
||||||
export const deletePost = (id: number) => del(`news/${id}`);
|
export const deletePost = (id: number) => del(`news/${id}`);
|
||||||
|
|||||||
Regular → Executable
+13
-12
@@ -1,24 +1,25 @@
|
|||||||
import { del, get, post, put } from "./api";
|
import { del, get, post, put } from "./api";
|
||||||
|
|
||||||
|
|
||||||
export const fetchNovels = (orderBy?: string) => get<NovelType[]>(orderBy ? `novels?sort=${orderBy}` : 'novels');
|
export const fetchNovels = (orderBy?: string) => get<Unmei.NovelType[]>(orderBy ? `novels?sort=${orderBy}` : 'novels');
|
||||||
export const createNovel = (novel: NovelType) => post<NovelType>('novels', { ...novel });
|
export const searchNovel = (query: string) => get<Unmei.NovelType[]>(`novels?q=${query}`);
|
||||||
|
export const createNovel = (novel: Unmei.NovelType) => post<Unmei.NovelType>('novels', novel);
|
||||||
|
|
||||||
export const fetchNovel = (id: number) => get<NovelType>(`novels/${id}`);
|
export const fetchNovel = (id: number) => get<Unmei.NovelType>(`novels/${id}`);
|
||||||
export const updateNovel = (novel: NovelType) => put<NovelType>(`novels/${novel.id}`, novel);
|
export const updateNovel = (novel: Unmei.NovelType) => put<Unmei.NovelType>(`novels/${novel.id}`, novel);
|
||||||
|
export const uploadNovelCover = (id: number, cover: FormData) => post<string>(`novels/${id}/cover`, cover);
|
||||||
|
|
||||||
export const fetchNovelCharacters = (id: number) => get<CharacterType[]>(`novels/${id}/characters`);
|
export const fetchNovelCharacters = (id: number) => get<Unmei.CharacterType[]>(`novels/${id}/characters`);
|
||||||
|
|
||||||
export const fetchGenres = () => get<GenreType[]>('genres');
|
export const fetchNovelGenres = (id: number) => get<Unmei.GenreType[]>(`novels/${id}/genres`);
|
||||||
export const fetchNovelGenres = (id: number) => get<GenreType[]>(`novels/${id}/genres`);
|
|
||||||
|
|
||||||
export const fetchNovelComments = (id: number, offset = 0, count = 5) => get<CommentsResponse>(`novels/${id}/comments?offset=${offset}&count=${count}`);
|
export const fetchNovelComments = (id: number, offset = 0, count = 5) => get<Unmei.CommentsResponse>(`novels/${id}/comments?offset=${offset}&count=${count}`);
|
||||||
export const postNovelComment = (id: number, text: string) => post<CommentType>(`novels/${id}/comments`, { text });
|
export const postNovelComment = (id: number, text: string) => post<Unmei.CommentType>(`novels/${id}/comments`, { text });
|
||||||
|
|
||||||
export const fetchUserNovel = (userId: number, novelId: number) => get<UserNovelType>(`users/${userId}/novels/${novelId}`);
|
export const fetchUserNovel = (userId: number, novelId: number) => get<Unmei.UserNovelType>(`users/${userId}/novels/${novelId}`);
|
||||||
export const createUserNovel = (userId: number, novelId: number, status = 'planned') => post<UserNovelType>(`users/${userId}/novels`, {
|
export const createUserNovel = (userId: number, novelId: number, status = 'planned') => post<Unmei.UserNovelType>(`users/${userId}/novels`, {
|
||||||
novel_id: Number(novelId),
|
novel_id: Number(novelId),
|
||||||
status
|
status
|
||||||
});
|
});
|
||||||
export const updateUserNovel = (userId: number, novelId: number, data: { mark?: number; status?: string }) => put<UserNovelType>(`users/${userId}/novels/${novelId}`, data);
|
export const updateUserNovel = (userId: number, novelId: number, data: { mark?: number; status?: string }) => put<Unmei.UserNovelType>(`users/${userId}/novels/${novelId}`, data);
|
||||||
export const deleteUserNovel = (userId: number, novelId: number) => del(`users/${userId}/novels/${novelId}`);
|
export const deleteUserNovel = (userId: number, novelId: number) => del(`users/${userId}/novels/${novelId}`);
|
||||||
|
|||||||
Regular → Executable
+8
-8
@@ -1,22 +1,22 @@
|
|||||||
import { get, post, put } from "./api";
|
import { get, post, put } from "./api";
|
||||||
|
|
||||||
export const fetchUsers = () => get<UserType[]>('users');
|
export const fetchUsers = () => get<Unmei.UserType[]>('users');
|
||||||
|
|
||||||
export const fetchUser = (id: number) => get<UserType>(`users/${id}`);
|
export const fetchUser = (id: number) => get<Unmei.UserType>(`users/${id}`);
|
||||||
export const updateUser = (user: UserType) => put<UserType>(`users/${user.id}`, { user });
|
export const updateUser = (user: Unmei.UserType) => put<Unmei.UserType>(`users/${user.id}`, { user });
|
||||||
|
|
||||||
export const fetchCurrentUser = () => get<UserType>('users/me');
|
export const fetchCurrentUser = () => get<Unmei.UserType>('users/me');
|
||||||
export const fetchUserNovels = (id: number) => get<NovelType[]>(`users/${id}/novels`);
|
export const fetchUserNovels = (id: number) => get<Unmei.NovelType[]>(`users/${id}/novels`);
|
||||||
|
|
||||||
export const activateAccount = (token: string) => post<string>('auth/activate', { token });
|
export const activateAccount = (token: string) => post<string>('auth/activate', { token });
|
||||||
export const generateActivateLink = () => post<string>('auth/activateToken');
|
export const generateActivateLink = () => post<string>('auth/activateToken');
|
||||||
|
|
||||||
export const fetchUserSettings = () => get<UserSettingsType>('users/me/settings');
|
export const fetchUserSettings = () => get<Unmei.UserSettingsType>('users/me/settings');
|
||||||
export const updateUserGeneralSettings = (use_gravatar: boolean, avatar: string) => post<UserType>('users/me/settings', {
|
export const updateUserGeneralSettings = (use_gravatar: boolean, avatar: string) => post<Unmei.UserType>('users/me/settings', {
|
||||||
use_gravatar,
|
use_gravatar,
|
||||||
avatar
|
avatar
|
||||||
});
|
});
|
||||||
export const updateUserAppearanceSettings = (theme: string) => post<UserType>('users/me/settings', { theme });
|
export const updateUserAppearanceSettings = (theme: string) => post<Unmei.UserType>('users/me/settings', { theme });
|
||||||
export const uploadAvatar = (avatar: FormData) => post<string>('users/me/avatar', avatar);
|
export const uploadAvatar = (avatar: FormData) => post<string>('users/me/avatar', avatar);
|
||||||
|
|
||||||
export const generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', {
|
export const generateRestoreLink = (email: string, recaptcha: string) => post('auth/restoreToken', {
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+34
-18
@@ -1,4 +1,4 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap')
|
@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@100;200;300;400;500;600;700;800;900&display=swap')
|
||||||
@import "../../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
html, body, #root
|
html, body, #root
|
||||||
@@ -6,7 +6,8 @@ html, body, #root
|
|||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
body, pre, textarea
|
body, pre, textarea
|
||||||
font-family: 'Roboto', sans-serif
|
font-family: var(--font-family), sans-serif
|
||||||
|
font-weight: var(--font-weight)
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, p, body, pre
|
h1, h2, h3, h4, h5, h6, p, body, pre
|
||||||
margin: 0
|
margin: 0
|
||||||
@@ -27,8 +28,20 @@ body
|
|||||||
--error-bg-color: #{$errorBackgroundColor}
|
--error-bg-color: #{$errorBackgroundColor}
|
||||||
--success-bg-color: #{$successBackgroundColor}
|
--success-bg-color: #{$successBackgroundColor}
|
||||||
|
|
||||||
|
--planned-bg-color: #399
|
||||||
|
--completed-bg-color: #393
|
||||||
|
--dropped-bg-color: #933
|
||||||
|
--in_progress-bg-color: #939
|
||||||
|
--deferred-bg-color: #333
|
||||||
|
|
||||||
--accent-color: #f22
|
--accent-color: #f22
|
||||||
|
|
||||||
|
--site-width: 1200px
|
||||||
|
--sidebar-width: 350px
|
||||||
|
|
||||||
|
--font-family: 'Rubik'
|
||||||
|
--font-weight: 400
|
||||||
|
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
background: var(--main-bg-color)
|
background: var(--main-bg-color)
|
||||||
|
|
||||||
@@ -67,17 +80,15 @@ body[theme='light']
|
|||||||
--block-sec-color: #{$blockSecondaryBackgroundColorLight}
|
--block-sec-color: #{$blockSecondaryBackgroundColorLight}
|
||||||
--nav-bg-color: #{$navbarBackgroundColorLight}
|
--nav-bg-color: #{$navbarBackgroundColorLight}
|
||||||
|
|
||||||
body[theme='win95']
|
*::-webkit-scrollbar
|
||||||
--main-color: #{$mainColorLight}
|
width: 6px
|
||||||
--link-color: #{$linkHoverColorLight}
|
|
||||||
--sec-color: #{$secondaryColorLight}
|
|
||||||
|
|
||||||
--main-bg-color: #008083
|
*::-webkit-scrollbar-track
|
||||||
--sec-bg-color: #c0c0c0
|
background-color: var(--nav-bg-color)
|
||||||
--nav-bg-color: #c0c0c0
|
|
||||||
|
|
||||||
.Navbar
|
*::-webkit-scrollbar-thumb
|
||||||
box-shadow: 0 1px 1px 1px white
|
background-color: var(--link-color)
|
||||||
|
outline: none
|
||||||
|
|
||||||
a
|
a
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
@@ -86,16 +97,21 @@ a
|
|||||||
&:hover
|
&:hover
|
||||||
color: var(--link-color)
|
color: var(--link-color)
|
||||||
|
|
||||||
*
|
|
||||||
scrollbar-color: #eee #222
|
|
||||||
scrollbar-width: thin
|
|
||||||
|
|
||||||
.Container
|
.Container
|
||||||
max-width: 1200px
|
position: relative
|
||||||
min-height: calc(100vh - 16rem)
|
max-width: var(--site-width)
|
||||||
margin: .5rem auto
|
min-height: calc(100vh - 240px - 48px)
|
||||||
|
padding: 8px 0
|
||||||
|
margin: 0 auto
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
.Notifications
|
.Notifications
|
||||||
|
width: 100%
|
||||||
|
position: fixed
|
||||||
|
bottom: 0
|
||||||
|
left: 0
|
||||||
|
z-index: 100000
|
||||||
|
|
||||||
@media (min-width: 500px)
|
@media (min-width: 500px)
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
Regular → Executable
+43
-72
@@ -1,39 +1,28 @@
|
|||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { Route, Switch } from "react-router-dom";
|
|
||||||
import './App.sass';
|
|
||||||
import Navbar from "../../components/Navbar/Navbar";
|
|
||||||
import Novel from "../Novel/Novel";
|
|
||||||
import User from "../User/User";
|
|
||||||
import Main from "../Main/Main"
|
|
||||||
import NotFoundError from "../../components/NotFoundError";
|
|
||||||
import { fetchCurrentUser, fetchUserSettings } from "../../api/users";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import Character from "../Character/Character";
|
import { withProfiler } from "@sentry/react";
|
||||||
import SpoilerTag from "../../ui/Spoiler/SpoilerTag";
|
|
||||||
import Novels from "../Novels/Novels";
|
import './App.sass';
|
||||||
import Footer from '../../components/Footer/Footer';
|
|
||||||
import UserNovels from '../User/Novels/UserNovels';
|
import { fetchCurrentUser, fetchUserSettings } from "../../api/users";
|
||||||
import ColorTag from "../../ui/Tags/ColorTag";
|
import { build, getVersion} from "../../api/api";
|
||||||
import ActivateAccount from "../ActivateAccount";
|
|
||||||
import AllNews from "../News/AllNews";
|
|
||||||
import Post from "../News/Post";
|
|
||||||
import AdminPanel from "../AdminPanel/AdminPanel";
|
|
||||||
import PasswordRestoreGenerate from "../PasswordRestore/PasswordRestoreGenerate";
|
|
||||||
import PasswordRestore from "../PasswordRestore/PasswordRestore";
|
|
||||||
import Users from "../Users/Users";
|
|
||||||
import { build, getVersion, version } from "../../api/api";
|
|
||||||
import Settings from "../User/Settings/Settings";
|
|
||||||
import { setUser } from "../../store/ducks/currentUser";
|
import { setUser } from "../../store/ducks/currentUser";
|
||||||
import { setSettings } from "../../store/ducks/userSettings";
|
import { setSettings } from "../../store/ducks/userSettings";
|
||||||
import { withProfiler } from "@sentry/react";
|
|
||||||
import { hasPermission, isMobile } from "../../utils";
|
import { SpoilerTag, ColorTag } from "../../ui/ui";
|
||||||
import Club from "../Club/Club";
|
import { hasPermission } from "../../utils";
|
||||||
import Snowfall from "react-snowfall";
|
import Preloader from "../Preloader/Preloader";
|
||||||
|
import Router from "./Router";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import eruda from 'eruda';
|
import eruda from 'eruda';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
import Clubs from "../Clubs/Clubs";
|
import Navbar from "../Navbar/Navbar";
|
||||||
|
|
||||||
|
const Snowfall = React.lazy(() => import("react-snowfall"));
|
||||||
|
// const Sidebar = React.lazy(() => import("../Sidebar/Sidebar"));
|
||||||
|
const Footer = React.lazy(() => import('../Footer/Footer'));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
notifications: React.ReactNode[]
|
notifications: React.ReactNode[]
|
||||||
@@ -41,10 +30,11 @@ type Props = {
|
|||||||
setUser: SetUser
|
setUser: SetUser
|
||||||
setSettings: SetSettings
|
setSettings: SetSettings
|
||||||
snowfall: Snowfall
|
snowfall: Snowfall
|
||||||
|
preloader: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
user: UserType | null
|
user: Unmei.UserType | null
|
||||||
};
|
};
|
||||||
|
|
||||||
class App extends React.Component<Props, State> {
|
class App extends React.Component<Props, State> {
|
||||||
@@ -59,7 +49,7 @@ class App extends React.Component<Props, State> {
|
|||||||
parser.registerTag('color', ColorTag);
|
parser.registerTag('color', ColorTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
const { setUser, setSettings } = this.props;
|
const { setUser, setSettings } = this.props;
|
||||||
|
|
||||||
const cachedTheme = localStorage.getItem('theme');
|
const cachedTheme = localStorage.getItem('theme');
|
||||||
@@ -91,16 +81,16 @@ class App extends React.Component<Props, State> {
|
|||||||
document.body.setAttribute('theme', settings.theme);
|
document.body.setAttribute('theme', settings.theme);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if(hasPermission(user, 'mobile_debug') && isMobile())
|
if(hasPermission(user, 'mobile_debug'))
|
||||||
eruda.init();
|
eruda.init();
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
if(err.code !== 3 && err.text)
|
if(err.code !== 3 && err.text)
|
||||||
console.error(err.text);
|
console.error(err.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
getVersion().then(res => {
|
getVersion().then(res => {
|
||||||
if(build != res.build && !/(dev\..+)/.test(window.location.href)) {
|
if(build !== res.build && !/(dev\..+)/.test(window.location.href)) {
|
||||||
console.debug(`Current back-end version: ${res.version} (${res.build}). Clearing cache...`);
|
console.debug(`Current back-end version: ${res.version}. Clearing cache`);
|
||||||
|
|
||||||
caches.keys().then(names => {
|
caches.keys().then(names => {
|
||||||
names.forEach(name => caches.delete(name).catch(console.error))
|
names.forEach(name => caches.delete(name).catch(console.error))
|
||||||
@@ -116,7 +106,7 @@ class App extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { modal, snowfall } = this.props;
|
const { modal, snowfall, preloader } = this.props;
|
||||||
const { user } = this.state;
|
const { user } = this.state;
|
||||||
const isBanned = user?.is_banned || JSON.parse(localStorage.getItem('banned') ?? 'false');
|
const isBanned = user?.is_banned || JSON.parse(localStorage.getItem('banned') ?? 'false');
|
||||||
|
|
||||||
@@ -130,10 +120,15 @@ class App extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar/>
|
{preloader && <Preloader/>}
|
||||||
{snowfall.snowfallStatus && (
|
<Suspense fallback={null}>
|
||||||
<Snowfall color={'white'} snowflakeCount={snowfall.snowflakeCount} style={{ zIndex: 1000, position: "fixed" }}/>
|
<Navbar/>
|
||||||
)}
|
</Suspense>
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
{snowfall.snowfallStatus && (
|
||||||
|
<Snowfall color={'white'} snowflakeCount={snowfall.snowflakeCount} style={{ zIndex: 1000, position: "fixed" }}/>
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
<div className={'Container'}>
|
<div className={'Container'}>
|
||||||
<div className="Notifications">
|
<div className="Notifications">
|
||||||
{this.props.notifications.map((n, i) => (
|
{this.props.notifications.map((n, i) => (
|
||||||
@@ -142,38 +137,13 @@ class App extends React.Component<Props, State> {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Switch>
|
{Router}
|
||||||
<Route exact path={'/'} component={Main}/>
|
|
||||||
|
|
||||||
<Route exact path={'/news'} component={AllNews}/>
|
|
||||||
<Route exact path={'/news/:postId'} component={Post}/>
|
|
||||||
|
|
||||||
<Route exact path={'/novels'} component={Novels}/>
|
|
||||||
<Route exact path={'/novels/:novelId'} component={Novel}/>
|
|
||||||
|
|
||||||
<Route exact path={'/clubs'} component={Clubs}/>
|
|
||||||
<Route exact path={'/clubs/:clubId'} component={Club}/>
|
|
||||||
|
|
||||||
<Route exact path={'/users'} component={Users}/>
|
|
||||||
<Route exact path={'/user/:userId'} component={User}/>
|
|
||||||
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
|
|
||||||
<Route exact path={'/user/:userId/settings'} component={Settings}/>
|
|
||||||
|
|
||||||
<Route exact path={'/character/:charId'} component={Character}/>
|
|
||||||
|
|
||||||
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
|
||||||
|
|
||||||
<Route exact path={'/restore'} component={PasswordRestoreGenerate}/>
|
|
||||||
<Route exact path={'/restore/:token'} component={PasswordRestore}/>
|
|
||||||
|
|
||||||
<Route path={'/kawaii__neko'} component={AdminPanel}/>
|
|
||||||
|
|
||||||
<Route component={NotFoundError}/>
|
|
||||||
</Switch>
|
|
||||||
|
|
||||||
{modal && modal}
|
{modal && modal}
|
||||||
</div>
|
</div>
|
||||||
<Footer/>
|
<Suspense fallback={null}>
|
||||||
|
<Footer/>
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -183,10 +153,11 @@ export default connect(
|
|||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
notifications: state.notifications,
|
notifications: state.notifications,
|
||||||
modal: state.modal,
|
modal: state.modal,
|
||||||
snowfall: state.snowfall
|
snowfall: state.snowfall,
|
||||||
|
preloader: state.preloader
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (userData: UserType) => dispatch(setUser(userData)),
|
setUser: (userData: Unmei.UserType) => dispatch(setUser(userData)),
|
||||||
setSettings: (settings: UserSettingsType) => dispatch(setSettings(settings))
|
setSettings: (settings: Unmei.UserSettingsType) => dispatch(setSettings(settings))
|
||||||
})
|
})
|
||||||
)(withProfiler(App));
|
)(withProfiler(App));
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import React, { Suspense } from "react";
|
||||||
|
import { Route, Switch } from "react-router-dom";
|
||||||
|
import Preloader from "../Preloader/Preloader";
|
||||||
|
|
||||||
|
const Main = React.lazy(() => import('../../pages/Main/Main'));
|
||||||
|
const AllNews = React.lazy(() => import('../../pages/News/AllNews'));
|
||||||
|
const Post = React.lazy(() => import('../../pages/News/Post'));
|
||||||
|
const Novels = React.lazy(() => import('../../pages/Novels/Novels'));
|
||||||
|
const Novel = React.lazy(() => import('../../pages/Novel/Novel'));
|
||||||
|
const Clubs = React.lazy(() => import('../../pages/Clubs/Clubs'));
|
||||||
|
const Club = React.lazy(() => import('../../pages/Club/Club'));
|
||||||
|
const Users = React.lazy(() => import('../../pages/Users/Users'));
|
||||||
|
const User = React.lazy(() => import('../../pages/User/User'));
|
||||||
|
const UserNovels = React.lazy(() => import('../../pages/User/Novels/UserNovels'));
|
||||||
|
const Settings = React.lazy(() => import('../../pages/User/Settings/Settings'));
|
||||||
|
const Character = React.lazy(() => import('../../pages/Character/Character'));
|
||||||
|
const ActivateAccount = React.lazy(() => import('../../pages/ActivateAccount'));
|
||||||
|
const PasswordRestoreGenerate = React.lazy(() => import('../../pages/PasswordRestore/PasswordRestoreGenerate'));
|
||||||
|
const PasswordRestore = React.lazy(() => import('../../pages/PasswordRestore/PasswordRestore'));
|
||||||
|
const AdminPanel = React.lazy(() => import('../../pages/AdminPanel/AdminPanel'));
|
||||||
|
const VKAuth = React.lazy(() => import('../../pages/VKAuth/VKAuth'));
|
||||||
|
const NotFoundError = React.lazy(() => import('../NotFound/NotFoundError'));
|
||||||
|
|
||||||
|
export default (
|
||||||
|
<Suspense fallback={<Preloader/>}>
|
||||||
|
<Switch>
|
||||||
|
<Route exact path={'/'} component={Main}/>
|
||||||
|
|
||||||
|
<Route exact path={'/news'} component={AllNews}/>
|
||||||
|
<Route exact path={'/news/:postId'} component={Post}/>
|
||||||
|
|
||||||
|
<Route exact path={'/novels'} component={Novels}/>
|
||||||
|
<Route exact path={'/novels/:novelId'} component={Novel}/>
|
||||||
|
|
||||||
|
<Route exact path={'/clubs'} component={Clubs}/>
|
||||||
|
<Route exact path={'/clubs/:clubId'} component={Club}/>
|
||||||
|
|
||||||
|
<Route exact path={'/users'} component={Users}/>
|
||||||
|
<Route exact path={'/user/:userId'} component={User}/>
|
||||||
|
<Route exact path={'/user/:userId/novels'} component={UserNovels}/>
|
||||||
|
<Route exact path={'/user/:userId/settings'} component={Settings}/>
|
||||||
|
|
||||||
|
<Route exact path={'/character/:charId'} component={Character}/>
|
||||||
|
|
||||||
|
<Route exact path={'/activate/:token'} component={ActivateAccount}/>
|
||||||
|
|
||||||
|
<Route exact path={'/restore'} component={PasswordRestoreGenerate}/>
|
||||||
|
<Route exact path={'/restore/:token'} component={PasswordRestore}/>
|
||||||
|
|
||||||
|
<Route exact path={'/vk_auth'} component={VKAuth}/>
|
||||||
|
|
||||||
|
<Route path={'/kawaii__neko'} component={AdminPanel}/>
|
||||||
|
|
||||||
|
<Route component={NotFoundError}/>
|
||||||
|
</Switch>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
Regular → Executable
+2
-3
@@ -5,6 +5,8 @@
|
|||||||
position: relative
|
position: relative
|
||||||
bottom: 0
|
bottom: 0
|
||||||
padding: 1rem
|
padding: 1rem
|
||||||
|
box-sizing: border-box
|
||||||
|
height: 240px
|
||||||
|
|
||||||
.Footer__Tabs
|
.Footer__Tabs
|
||||||
display: flex
|
display: flex
|
||||||
@@ -55,8 +57,5 @@
|
|||||||
|
|
||||||
.Footer__Bottom
|
.Footer__Bottom
|
||||||
border-top: 1px solid var(--main-color)
|
border-top: 1px solid var(--main-color)
|
||||||
display: flex
|
|
||||||
justify-content: center
|
|
||||||
flex-direction: column
|
|
||||||
padding: 2rem 0
|
padding: 2rem 0
|
||||||
text-align: center
|
text-align: center
|
||||||
Regular → Executable
+16
-16
@@ -1,45 +1,45 @@
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import styles from './Footer.module.sass';
|
import './Footer.sass';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faDiscord, faTelegramPlane, faVk } from '@fortawesome/free-brands-svg-icons';
|
import { faDiscord, faTelegramPlane, faVk } from '@fortawesome/free-brands-svg-icons';
|
||||||
import { build, getVersion, version } from "../../api/api";
|
import { build, version, getVersion } from "../../api/api";
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
const Footer: React.FC = () => {
|
||||||
const [backend, setVersion] = useState<VersionResponse>({ version: '', build: 0 });
|
const [backend, setVersion] = useState<Unmei.VersionResponse>({ version: '', build: 0 });
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
getVersion().then(setVersion);
|
getVersion().then(setVersion);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.Footer}>
|
<div className='Footer'>
|
||||||
<footer className={styles.Footer__Tabs}>
|
<footer className='Footer__Tabs'>
|
||||||
<div className={styles.Footer__Tab}>
|
<div className="Footer__Tab">
|
||||||
<div className={styles.Footer__Tab_Title}>Команда</div>
|
<div className="Footer__Tab_Title">Команда</div>
|
||||||
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
<Link to={'/user/1'} className={'Link'}>Nix13</Link>
|
||||||
<Link to={'/user/6'} className={'Link'}>RonFall</Link>
|
<Link to={'/user/6'} className={'Link'}>RonFall</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.Footer__Tab}>
|
<div className="Footer__Tab">
|
||||||
<div className={styles.Footer__Tab_Title}>Соцсети</div>
|
<div className="Footer__Tab_Title">Соцсети</div>
|
||||||
|
|
||||||
<div className={styles.Links}>
|
<div className="Links">
|
||||||
<a href="https://vk.com/unmei_site">
|
<a href="https://vk.com/unmei_site">
|
||||||
<div className={styles.Link}><FontAwesomeIcon icon={faVk}/></div>
|
<div className="Link"><FontAwesomeIcon icon={faVk} title='VK' /></div>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://discord.gg/4CA8Cju">
|
<a href="https://discord.gg/4CA8Cju">
|
||||||
<div className={styles.Link}><FontAwesomeIcon icon={faDiscord}/></div>
|
<div className="Link"><FontAwesomeIcon icon={faDiscord} title='Discord'/></div>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://t.me/unmei_site">
|
<a href="https://t.me/unmei_site">
|
||||||
<div className={styles.Link}><FontAwesomeIcon icon={faTelegramPlane}/></div>
|
<div className="Link"><FontAwesomeIcon icon={faTelegramPlane} title='Telegram'/></div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
<div className={styles.Footer__Bottom}>
|
<div className="Footer__Bottom">
|
||||||
<div>Unmei © 2019-2021</div>
|
<div>Unmei © 2019-2021</div>
|
||||||
<div>
|
<div>
|
||||||
Frontend version: <span style={{ color: backend.build == build ? "green" : "red" }}>{version} ({build})</span>;
|
Frontend version: <span style={{ color: backend.build === build ? "green" : "red" }}>{version} ({build})</span>;
|
||||||
Backend version: <span style={{ color: backend.build == build ? "green" : "red" }}>{backend.version} ({backend.build})</span>
|
Backend version: <span style={{ color: backend.build === build ? "green" : "red" }}>{backend.version} ({backend.build})</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+9
-5
@@ -1,8 +1,8 @@
|
|||||||
.Minimized
|
.Minimized
|
||||||
height: 3rem
|
height: 48px
|
||||||
|
|
||||||
.Expanded
|
.Expanded
|
||||||
min-height: 3rem
|
min-height: 48px
|
||||||
|
|
||||||
&__Links
|
&__Links
|
||||||
position: relative
|
position: relative
|
||||||
@@ -15,15 +15,16 @@
|
|||||||
justify-content: space-between
|
justify-content: space-between
|
||||||
align-items: flex-start
|
align-items: flex-start
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
padding: 0 8px
|
||||||
|
|
||||||
&__Links, &__User
|
&__Links, &__User
|
||||||
min-height: 3rem
|
min-height: 48px
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
|
||||||
&_Button
|
&_Button
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
padding: .5rem
|
padding: 4px
|
||||||
color: var(--main-color)
|
color: var(--main-color)
|
||||||
background-color: var(--nav-bg-color)
|
background-color: var(--nav-bg-color)
|
||||||
border: none
|
border: none
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
align-items: center
|
align-items: center
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
position: relative
|
position: relative
|
||||||
|
font-weight: 300
|
||||||
|
|
||||||
&::before
|
&::before
|
||||||
content: ""
|
content: ""
|
||||||
@@ -62,9 +64,10 @@
|
|||||||
position: absolute
|
position: absolute
|
||||||
background-color: var(--nav-bg-color)
|
background-color: var(--nav-bg-color)
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
padding: .5rem
|
padding: 4px
|
||||||
max-width: 250px
|
max-width: 250px
|
||||||
right: 0
|
right: 0
|
||||||
|
z-index: 100000
|
||||||
|
|
||||||
&_Item
|
&_Item
|
||||||
margin: .2rem 0
|
margin: .2rem 0
|
||||||
@@ -87,6 +90,7 @@
|
|||||||
border-radius: 0
|
border-radius: 0
|
||||||
padding: 0 .5rem
|
padding: 0 .5rem
|
||||||
position: relative
|
position: relative
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
&::before
|
&::before
|
||||||
content: ""
|
content: ""
|
||||||
|
|||||||
Regular → Executable
+1
-1
@@ -14,7 +14,7 @@ import { setModal } from "../../store/ducks/modal";
|
|||||||
type Props = {
|
type Props = {
|
||||||
setModal: SetModal
|
setModal: SetModal
|
||||||
logout: LogoutUser
|
logout: LogoutUser
|
||||||
currentUser: UserType
|
currentUser: Unmei.UserType
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
|||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
function NotFoundError() {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', width: '100%', height: '100%' }}>
|
||||||
|
<div style={{ fontSize: '2rem', fontWeight: 600 }}>Увы, данная страница не найдена!</div>
|
||||||
|
<div>Проверьте правильность написания ссылки!</div>
|
||||||
|
<img src="/static/img/not-found.png" style={{ width: 300, height: 400 }} alt=""/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFoundError;
|
||||||
Regular → Executable
@@ -0,0 +1,54 @@
|
|||||||
|
#Preloader
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
background: var(--main-bg-color)
|
||||||
|
z-index: 10000
|
||||||
|
|
||||||
|
#Loader
|
||||||
|
display: block
|
||||||
|
position: relative
|
||||||
|
left: 50%
|
||||||
|
top: 50%
|
||||||
|
width: 150px
|
||||||
|
height: 150px
|
||||||
|
margin: -75px 0 0 -75px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 3px solid transparent
|
||||||
|
border-top-color: #9370DB
|
||||||
|
-webkit-animation: spin 2s linear infinite
|
||||||
|
animation: spin 2s linear infinite
|
||||||
|
|
||||||
|
&:before
|
||||||
|
content: ""
|
||||||
|
position: absolute
|
||||||
|
top: 5px
|
||||||
|
left: 5px
|
||||||
|
right: 5px
|
||||||
|
bottom: 5px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 3px solid transparent
|
||||||
|
border-top-color: #BA55D3
|
||||||
|
-webkit-animation: spin 3s linear infinite
|
||||||
|
animation: spin 3s linear infinite
|
||||||
|
|
||||||
|
&:after
|
||||||
|
content: ""
|
||||||
|
position: absolute
|
||||||
|
top: 15px
|
||||||
|
left: 15px
|
||||||
|
right: 15px
|
||||||
|
bottom: 15px
|
||||||
|
border-radius: 50%
|
||||||
|
border: 3px solid transparent
|
||||||
|
border-top-color: #FF00FF
|
||||||
|
-webkit-animation: spin 1.5s linear infinite
|
||||||
|
animation: spin 1.5s linear infinite
|
||||||
|
|
||||||
|
@keyframes spin
|
||||||
|
0%
|
||||||
|
transform: rotate(0deg)
|
||||||
|
100%
|
||||||
|
transform: rotate(360deg)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import './Preloader.sass';
|
||||||
|
//import './PreloaderTest.sass';
|
||||||
|
|
||||||
|
const Preloader = () => (
|
||||||
|
<div id={'Preloader'}>
|
||||||
|
<div id="Loader"/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Preloader;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@keyframes preloader
|
||||||
|
0%
|
||||||
|
background-position: 0% 50%
|
||||||
|
50%
|
||||||
|
background-position: 100% 50%
|
||||||
|
100%
|
||||||
|
background-position: 0% 50%
|
||||||
|
|
||||||
|
#Preloader
|
||||||
|
height: 100vh
|
||||||
|
width: 100vw
|
||||||
|
position: fixed
|
||||||
|
z-index: 10000
|
||||||
|
background-color: #6b0f1a
|
||||||
|
background-image: linear-gradient(315deg, #6b0f1a 0%, #b91372 74%)
|
||||||
|
background-size: 400%
|
||||||
|
animation: preloader 5s ease-in-out
|
||||||
|
animation-iteration-count: infinite
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
@keyframes slideOut
|
||||||
|
0%
|
||||||
|
left: -350px
|
||||||
|
100%
|
||||||
|
left: 0
|
||||||
|
|
||||||
|
@keyframes slideIn
|
||||||
|
0%
|
||||||
|
left: 0
|
||||||
|
100%
|
||||||
|
left: -350px
|
||||||
|
|
||||||
|
.OpenSidebar, .CloseSidebar
|
||||||
|
position: fixed
|
||||||
|
width: 32px
|
||||||
|
height: 32px
|
||||||
|
top: 8px
|
||||||
|
z-index: 1000
|
||||||
|
background-color: var(--nav-bg-color)
|
||||||
|
padding: 8px
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
.OpenSidebar
|
||||||
|
left: 8px
|
||||||
|
|
||||||
|
.CloseSidebar
|
||||||
|
@media screen and (max-width: 400px)
|
||||||
|
left: calc(100vw - 32px)
|
||||||
|
|
||||||
|
left: calc(var(--sidebar-width) - 32px)
|
||||||
|
animation: slideOut 500ms linear
|
||||||
|
|
||||||
|
.Sidebar
|
||||||
|
box-sizing: border-box
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
height: 100%
|
||||||
|
background-color: var(--nav-bg-color)
|
||||||
|
z-index: 10
|
||||||
|
animation: slideOut 500ms linear
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
@media (min-width: 400px)
|
||||||
|
width: var(--sidebar-width)
|
||||||
|
|
||||||
|
&_Hide
|
||||||
|
animation: slideIn 500ms linear
|
||||||
|
|
||||||
|
&__Close
|
||||||
|
font-size: 20px
|
||||||
|
position: absolute
|
||||||
|
top: 8px
|
||||||
|
right: 8px
|
||||||
|
|
||||||
|
&__Button
|
||||||
|
padding: .5rem
|
||||||
|
color: var(--main-color)
|
||||||
|
background-color: var(--block-color)
|
||||||
|
width: 100%
|
||||||
|
text-align: center
|
||||||
|
cursor: pointer
|
||||||
|
border: none
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
|
|
||||||
|
&__Auth, &__User
|
||||||
|
border-bottom: var(--block-color) 1px solid
|
||||||
|
padding: 8px
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
&__Auth
|
||||||
|
width: 100%
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.Sidebar__Button
|
||||||
|
font-size: 1.2rem !important
|
||||||
|
|
||||||
|
&__User
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
&_Buttons
|
||||||
|
display: flex
|
||||||
|
|
||||||
|
&_Info
|
||||||
|
margin-left: 8px
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
&_Name
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 900
|
||||||
|
|
||||||
|
&_Group
|
||||||
|
border-color: #eeeeee
|
||||||
|
border-width: 1px
|
||||||
|
border-style: solid
|
||||||
|
color: var(--main-color)
|
||||||
|
|
||||||
|
border-radius: 2px
|
||||||
|
padding: .2rem
|
||||||
|
text-align: center
|
||||||
|
font-size: 1rem
|
||||||
|
font-weight: 300
|
||||||
|
position: relative
|
||||||
|
z-index: 1
|
||||||
|
margin: 4px 0
|
||||||
|
|
||||||
|
&::before
|
||||||
|
content: ''
|
||||||
|
width: 100%
|
||||||
|
border-top-color: inherit
|
||||||
|
border-top-width: 27px
|
||||||
|
border-top-style: solid
|
||||||
|
opacity: 0.5
|
||||||
|
position: absolute
|
||||||
|
z-index: -1
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
|
||||||
|
.Button
|
||||||
|
margin-top: 8px
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
&_Avatar
|
||||||
|
width: 80px
|
||||||
|
height: 80px
|
||||||
|
max-width: 80px
|
||||||
|
max-height: 80px
|
||||||
|
border-radius: 50%
|
||||||
|
|
||||||
|
&__Links
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
&__Link
|
||||||
|
cursor: pointer
|
||||||
|
font-size: 1.5rem
|
||||||
|
width: 100%
|
||||||
|
background-color: var(--main-bg-color)
|
||||||
|
box-sizing: border-box
|
||||||
|
padding: 4px 4px 4px 16px
|
||||||
|
position: relative
|
||||||
|
transition: background-color 200ms
|
||||||
|
|
||||||
|
&::before
|
||||||
|
content: ""
|
||||||
|
width: 100%
|
||||||
|
position: absolute
|
||||||
|
bottom: 0
|
||||||
|
left: 0
|
||||||
|
height: 0
|
||||||
|
background: var(--accent-color)
|
||||||
|
transition: heigth 200ms linear
|
||||||
|
-webkit-transition: height .1s linear
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--main-bg-color)
|
||||||
|
|
||||||
|
&::before
|
||||||
|
height: 2px
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
background-color: var(--sec-bg-color)
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import React from "react";
|
||||||
|
import './Sidebar.sass';
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setModal } from "../../store/ducks/modal";
|
||||||
|
import LoginModal from "../../pages/Modals/LoginModal";
|
||||||
|
import { logout } from "../../store/ducks/currentUser";
|
||||||
|
import RegisterModal from "../../pages/Modals/RegisterModal";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faAlignJustify, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { hasAccessToAdminPanel } from "../../utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
modal: React.ReactNode | null
|
||||||
|
user: Unmei.UserType
|
||||||
|
setModal: SetModal
|
||||||
|
logout: LogoutUser
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
closed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sidebar extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
closed: true
|
||||||
|
}
|
||||||
|
private sidebar: HTMLDivElement | null | undefined;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('keyup', this.handleKeyUp, false);
|
||||||
|
document.addEventListener('mousedown', this.handleOutsideClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('keyup', this.handleKeyUp, false);
|
||||||
|
document.removeEventListener('mousedown', this.handleOutsideClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if(e.code === 'Escape' && !this.props.modal) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.closeSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOutsideClick = (e: any) => {
|
||||||
|
if(this.sidebar && !this.sidebar.contains(e.target) && !this.props.modal) {
|
||||||
|
this.closeSidebar()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
closeSidebar = () => {
|
||||||
|
const closeButton = document.getElementById('CloseSidebar');
|
||||||
|
if(!this.sidebar) return;
|
||||||
|
if(!closeButton) return;
|
||||||
|
|
||||||
|
this.sidebar.classList.add('Sidebar_Hide');
|
||||||
|
closeButton.classList.add('Sidebar_Hide');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ closed: true });
|
||||||
|
}, 450);
|
||||||
|
}
|
||||||
|
|
||||||
|
openSidebar = () => {
|
||||||
|
this.setState({ closed: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { user, setModal, logout } = this.props;
|
||||||
|
const { closed } = this.state;
|
||||||
|
|
||||||
|
const userNameStyle: React.CSSProperties = JSON.parse(localStorage.getItem('color-name') || 'false') && user.group && user.group.id !== 0 ? { color: user.group.color } : {};
|
||||||
|
|
||||||
|
if(closed)
|
||||||
|
return <div onClick={this.openSidebar} className='OpenSidebar'><FontAwesomeIcon icon={faAlignJustify}/></div>;
|
||||||
|
return (<>
|
||||||
|
<div className="Sidebar" ref={ref => this.sidebar=ref}>
|
||||||
|
<div onClick={this.closeSidebar} className='CloseSidebar' id='CloseSidebar'><FontAwesomeIcon icon={faTimesCircle}/></div>
|
||||||
|
|
||||||
|
{user.authorized ? (
|
||||||
|
<div className="Sidebar__User">
|
||||||
|
<img className='Sidebar__User_Avatar' src={user.avatar} alt={user.username}/>
|
||||||
|
<div className='Sidebar__User_Info'>
|
||||||
|
<Link to={`/user/${user.id}`} onClick={this.closeSidebar} className='Sidebar__User_Info_Name' style={userNameStyle}>{user.username}</Link>
|
||||||
|
<div className='Sidebar__User_Info_Group' style={{ borderColor: user.group.color }}>
|
||||||
|
{user.group.name}
|
||||||
|
</div>
|
||||||
|
<div className="Sidebar__User_Buttons">
|
||||||
|
<Link to={`/user/${user.id}/settings`} onClick={this.closeSidebar} className={'Sidebar__Button'}>Настройки</Link>
|
||||||
|
{user.group && hasAccessToAdminPanel(user) && <Link to={`/kawaii__neko`} onClick={this.closeSidebar} className={'Sidebar__Button'}>АП</Link>}
|
||||||
|
<button onClick={logout} className={'Sidebar__Button'}>Выход</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="Sidebar__Auth">
|
||||||
|
<button onClick={() => setModal(<LoginModal/>)} className={'Sidebar__Button'}>Войти</button>
|
||||||
|
<button onClick={() => setModal(<RegisterModal/>)} className={'Sidebar__Button'}>Регистрация</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='Sidebar__Links'>
|
||||||
|
<Link className='Sidebar__Link' to={'/'} onClick={this.closeSidebar}>Главная</Link>
|
||||||
|
<Link className='Sidebar__Link' to={'/novels'} onClick={this.closeSidebar}>Новеллы</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
(state: StoreState) => ({
|
||||||
|
user: state.currentUser,
|
||||||
|
modal: state.modal
|
||||||
|
}),
|
||||||
|
dispatch => ({
|
||||||
|
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
|
logout: () => dispatch(logout())
|
||||||
|
})
|
||||||
|
)(Sidebar);
|
||||||
Regular → Executable
+4
-5
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { BrowserRouter} from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import store from "./store/store";
|
import store from "./store/store";
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import App from './pages/App/App';
|
import App from './components/App/App';
|
||||||
import * as serviceWorker from './serviceWorker';
|
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||||
|
|
||||||
if(process.env.NODE_ENV === 'production')
|
if(process.env.NODE_ENV === 'production')
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
@@ -20,5 +20,4 @@ ReactDOM.render(
|
|||||||
</BrowserRouter>,
|
</BrowserRouter>,
|
||||||
document.getElementById('root'));
|
document.getElementById('root'));
|
||||||
|
|
||||||
// Better not disable this
|
serviceWorkerRegistration.register();
|
||||||
serviceWorker.register();
|
|
||||||
|
|||||||
Regular → Executable
+1
-1
@@ -30,7 +30,7 @@ class ActivateAccount extends React.Component<Props, State> {
|
|||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
this.props.history.push('/')
|
this.props.history.push('/')
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
const msg = errors[err.code];
|
const msg = errors[err.code];
|
||||||
if(msg) {
|
if(msg) {
|
||||||
this.setState({ msg, error: true });
|
this.setState({ msg, error: true });
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+12
-4
@@ -1,6 +1,6 @@
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import NotFoundError from "../../components/NotFoundError";
|
import NotFoundError from "../../components/NotFound/NotFoundError";
|
||||||
import { hasAccessToAdminPanel } from "../../utils";
|
import { hasAccessToAdminPanel } from "../../utils";
|
||||||
import './AdminPanel.sass';
|
import './AdminPanel.sass';
|
||||||
import { Link, Route, Switch } from "react-router-dom";
|
import { Link, Route, Switch } from "react-router-dom";
|
||||||
@@ -11,14 +11,17 @@ import APNovels from "./Novels/APNovels";
|
|||||||
import APUsers from "./Users/APUsers";
|
import APUsers from "./Users/APUsers";
|
||||||
import APModifyUser from "./Users/APModifyUser";
|
import APModifyUser from "./Users/APModifyUser";
|
||||||
import APModifyNovel from "./Novels/APModifyNovel";
|
import APModifyNovel from "./Novels/APModifyNovel";
|
||||||
|
import APCharacters from "./Characters/APCharacters";
|
||||||
|
import APModifyCharacter from "./Characters/APModifyCharacter";
|
||||||
import APMain from "./Main/APMain";
|
import APMain from "./Main/APMain";
|
||||||
|
import APAddNovel from "./Novels/APAddNovel";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
user: UserType
|
user: Unmei.UserType
|
||||||
match: { path: string }
|
match: { path: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {};
|
type State = {}
|
||||||
|
|
||||||
class AdminPanel extends React.Component<Props, State> {
|
class AdminPanel extends React.Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
@@ -29,11 +32,13 @@ class AdminPanel extends React.Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<div className={'AdminPanel'}>
|
<div className={'AdminPanel'}>
|
||||||
<div className="AdminPanel__Title">Админ панель</div>
|
<div className="AdminPanel__Title">Админ панель</div>
|
||||||
|
|
||||||
<div className="AdminPanel__Router">
|
<div className="AdminPanel__Router">
|
||||||
<Link to={path}>Главная</Link>
|
<Link to={path}>Главная</Link>
|
||||||
<Link to={`${path}/users`}>Пользователи</Link>
|
<Link to={`${path}/users`}>Пользователи</Link>
|
||||||
<Link to={`${path}/novels`}>Новелы</Link>
|
<Link to={`${path}/novels`}>Новелы</Link>
|
||||||
<Link to={`${path}/news`}>Новости</Link>
|
<Link to={`${path}/news`}>Новости</Link>
|
||||||
|
<Link to={`${path}/characters`}>Персонажи</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Switch>
|
<Switch>
|
||||||
@@ -44,12 +49,15 @@ class AdminPanel extends React.Component<Props, State> {
|
|||||||
<Route exact path={`${path}/news/:id`} component={APModifyPost}/>
|
<Route exact path={`${path}/news/:id`} component={APModifyPost}/>
|
||||||
|
|
||||||
<Route exact path={`${path}/novels`} component={APNovels}/>
|
<Route exact path={`${path}/novels`} component={APNovels}/>
|
||||||
<Route exact path={`${path}/novels/new`} component={NotFoundError}/>
|
<Route exact path={`${path}/novels/new`} component={APAddNovel}/>
|
||||||
<Route exact path={`${path}/novels/:id`} component={APModifyNovel}/>
|
<Route exact path={`${path}/novels/:id`} component={APModifyNovel}/>
|
||||||
|
|
||||||
<Route exact path={`${path}/users`} component={APUsers}/>
|
<Route exact path={`${path}/users`} component={APUsers}/>
|
||||||
<Route exact path={`${path}/users/new`} component={NotFoundError}/>
|
<Route exact path={`${path}/users/new`} component={NotFoundError}/>
|
||||||
<Route exact path={`${path}/users/:id`} component={APModifyUser}/>
|
<Route exact path={`${path}/users/:id`} component={APModifyUser}/>
|
||||||
|
|
||||||
|
<Route exact path={`${path}/characters`} component={APCharacters}/>
|
||||||
|
<Route exact path={`${path}/characters/:id`} component={APModifyCharacter}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
.APCharacter
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.Input, .TextField, .Button
|
||||||
|
font-size: 1.2rem
|
||||||
|
|
||||||
|
.Input, .TextField
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
&__Description
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
&_Preview, .TextField
|
||||||
|
color: var(--main-color) !important
|
||||||
|
width: 100%
|
||||||
|
height: 10rem
|
||||||
|
resize: vertical
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import { fetchChars } from "../../../api/characters";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
match: { path: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
characters: Unmei.CharacterType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
class APCharacters extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
characters: []
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetchChars().then(characters => this.setState({ characters }));
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { characters } = this.state;
|
||||||
|
const { match: { path } } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group title={'Персонажи'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Персонажи</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Link to={`${path}/new`}>
|
||||||
|
<Button>Добавить персонажа</Button>
|
||||||
|
</Link>
|
||||||
|
{characters.map(char => (
|
||||||
|
<Link to={`${path}/${char.id}`} key={char.id}>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{char.id}</div>
|
||||||
|
<div>{char.original_name}</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default APCharacters;
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import React, { ChangeEvent } from "react";
|
||||||
|
import Input from "../../../ui/Input/Input";
|
||||||
|
import Title from "../../../ui/Title/Title";
|
||||||
|
import BBEditor from "../../../ui/BBEditor/BBEditor";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import NotFoundError from "../../../components/NotFound/NotFoundError";
|
||||||
|
// @ts-ignore
|
||||||
|
import parser from 'bbcode-to-react';
|
||||||
|
import ConfirmPopout from "../../../ui/ConfirmPopout/ConfirmPopout";
|
||||||
|
import { deleteChar, fetchChar, updateChar } from "../../../api/characters";
|
||||||
|
import './APCharacters.sass';
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setPreloader } from "../../../store/ducks/preloader";
|
||||||
|
import { setModal } from "../../../store/ducks/modal";
|
||||||
|
import { addNotification } from "../../../store/ducks/notifications";
|
||||||
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
|
import TextField from "../../../ui/TextField/TextField";
|
||||||
|
import errors from "../../../api/errors";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
match: { params: { id: number } }
|
||||||
|
history: { push: (path: string) => void }
|
||||||
|
setPopout: SetModal
|
||||||
|
addNotification: AddNotification
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
char: Unmei.CharacterType | null
|
||||||
|
originalName: string
|
||||||
|
previewDescription: string
|
||||||
|
};
|
||||||
|
|
||||||
|
class APModifyCharacter extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
char: null, originalName: '', previewDescription: ''
|
||||||
|
}
|
||||||
|
private description = React.createRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { match: { params: { id } }, setPreloader } = this.props;
|
||||||
|
|
||||||
|
setPreloader(true);
|
||||||
|
fetchChar(id).then(char => {
|
||||||
|
this.setState({ char });
|
||||||
|
setPreloader(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveCharacter = (e: React.FormEvent) => {
|
||||||
|
const { char } = this.state;
|
||||||
|
const { addNotification } = this.props;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
if(!char) {
|
||||||
|
addNotification(
|
||||||
|
<NotificationMessage>
|
||||||
|
Персонаж - null!
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateChar(char).then(char => {
|
||||||
|
addNotification(
|
||||||
|
<NotificationMessage level={'success'}>
|
||||||
|
Персонаж #{char.id} сохранён!
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
this.setState({ char });
|
||||||
|
}).catch((err: Unmei.ApiError) => {
|
||||||
|
addNotification(
|
||||||
|
<NotificationMessage level={'error'}>
|
||||||
|
{errors[err.code] ?? err.text}
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const char = Object.assign({}, this.state.char);
|
||||||
|
const target = event.target;
|
||||||
|
// @ts-ignore
|
||||||
|
char[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||||
|
this.setState({ char });
|
||||||
|
}
|
||||||
|
|
||||||
|
previewDescription = () => {
|
||||||
|
if(!this.description.current) return;
|
||||||
|
this.setState({ previewDescription: parser.toReact(this.description.current.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChar = async (postId: number) => {
|
||||||
|
await deleteChar(postId);
|
||||||
|
this.props.history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
_deleteChar = () => {
|
||||||
|
const { match: { params: { id} }, setPopout } = this.props;
|
||||||
|
|
||||||
|
const popout = (
|
||||||
|
<ConfirmPopout onConfirm={() => this.deleteChar(id)}>
|
||||||
|
Вы уверены?
|
||||||
|
</ConfirmPopout>
|
||||||
|
);
|
||||||
|
|
||||||
|
setPopout(popout);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { char, originalName, previewDescription } = this.state;
|
||||||
|
|
||||||
|
if(!char) return <NotFoundError/>;
|
||||||
|
return (
|
||||||
|
<Group title={originalName}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{char.original_name} / Админ-панель</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<form onSubmit={this.saveCharacter} className={'APCharacter'}>
|
||||||
|
<Input type="text" placeholder={'Оригинальное имя'} value={char.original_name} onChange={this.onChange} name={'original_name'}/>
|
||||||
|
|
||||||
|
<div className={'APCharacter__Description'}>
|
||||||
|
<Title>Описание</Title>
|
||||||
|
<BBEditor inputRef={this.description} value={char.description} onChange={this.onChange} placeholder={'Описание'} name={'description'}/>
|
||||||
|
{previewDescription.length > 0 && (<>
|
||||||
|
<Title>Предпросмотр</Title>
|
||||||
|
<TextField className={'APCharacter__Description_Preview'} value={previewDescription} onChange={()=>{}}/>
|
||||||
|
</>)}
|
||||||
|
<Button type={"button"} onClick={this.previewDescription}>Предпросмотр</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button style={{ marginRight: '1rem' }}>Сохранить</Button>
|
||||||
|
<Button type={"button"} style={{ color: 'var(--error-bg-color)' }} onClick={this._deleteChar}>Удалить</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
null,
|
||||||
|
dispatch => ({
|
||||||
|
setPreloader: (s: boolean) => dispatch(setPreloader(s)),
|
||||||
|
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||||
|
})
|
||||||
|
)(APModifyCharacter);
|
||||||
@@ -2,9 +2,10 @@ import React from "react";
|
|||||||
import Group from "../../../ui/Group/Group";
|
import Group from "../../../ui/Group/Group";
|
||||||
import { build, getVersion, version } from "../../../api/api";
|
import { build, getVersion, version } from "../../../api/api";
|
||||||
import Loading from "../../../components/Loading";
|
import Loading from "../../../components/Loading";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
backend: VersionResponse | null
|
backend: Unmei.VersionResponse | null
|
||||||
}
|
}
|
||||||
|
|
||||||
class APMain extends React.Component<any, State> {
|
class APMain extends React.Component<any, State> {
|
||||||
@@ -22,14 +23,18 @@ class APMain extends React.Component<any, State> {
|
|||||||
if(!backend) return <Loading/>;
|
if(!backend) return <Loading/>;
|
||||||
return (
|
return (
|
||||||
<Group title={'Основное'}>
|
<Group title={'Основное'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Главное / Админ-панель</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Текущая версия сайта: <span style={{ color: backend.build == build ? "green" : "red" }}>{version} (билд: {build})</span>
|
Текущая версия сайта: <span style={{ color: backend.build === build ? "green" : "red" }}>{version} (билд: {build})</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Текущая версия API: <span style={{ color: backend.build == build ? "green" : "red" }}>{backend.version} (билд: {backend.build})</span>
|
Текущая версия API: <span style={{ color: backend.build === build ? "green" : "red" }}>{backend.version} (билд: {backend.build})</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Совпадение версий сборок: <span style={{ color: backend.build == build ? "green" : "red" }}>{backend.build == build ? 'да' : 'нет'}</span>
|
Совпадение версий сборок: <span style={{ color: backend.build === build ? "green" : "red" }}>{backend.build === build ? 'да' : 'нет'}</span>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,25 +13,24 @@ import { connect } from "react-redux";
|
|||||||
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
import { setModal } from "../../../store/ducks/modal";
|
import { setModal } from "../../../store/ducks/modal";
|
||||||
import { addNotification } from "../../../store/ducks/notifications";
|
import { addNotification } from "../../../store/ducks/notifications";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { path: string }
|
|
||||||
history: { push: (path: string) => void }
|
history: { push: (path: string) => void }
|
||||||
setPopout: (modal: React.ReactNode | null) => void
|
setPopout: (modal: React.ReactNode | null) => void
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
post: PostType
|
post: Unmei.PostType
|
||||||
title: string
|
|
||||||
|
|
||||||
previewShort: string
|
previewShort: string
|
||||||
previewFull: string
|
previewFull: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class APModifyPost extends React.Component<Props, State> {
|
class APAddPost extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
post: {} as PostType, title: '',
|
post: {} as Unmei.PostType,
|
||||||
previewShort: '', previewFull: ''
|
previewShort: '', previewFull: ''
|
||||||
}
|
}
|
||||||
private shortPost = React.createRef<HTMLTextAreaElement>();
|
private shortPost = React.createRef<HTMLTextAreaElement>();
|
||||||
@@ -55,8 +54,7 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
</NotificationMessage>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
addNotification(notification);
|
addNotification(notification);
|
||||||
const path = this.props.match.path.split('/');
|
this.props.history.push('/');
|
||||||
this.props.history.push(`/${path[1]}/${path[2]}`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +76,14 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { post, title, previewShort, previewFull } = this.state;
|
const { post, previewShort, previewFull } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={title}>
|
<Group title='Новая новость'>
|
||||||
|
<Helmet>
|
||||||
|
<title>{post.title ? `${post.title} / Админ-панель` : 'Новая новость'}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<form onSubmit={this.savePost} className={'APPost'}>
|
<form onSubmit={this.savePost} className={'APPost'}>
|
||||||
<Input type="text" placeholder={'Заголовок'} value={post.title ?? ''} onChange={this.changeTitle}/>
|
<Input type="text" placeholder={'Заголовок'} value={post.title ?? ''} onChange={this.changeTitle}/>
|
||||||
|
|
||||||
@@ -119,4 +121,4 @@ export default connect(null,
|
|||||||
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||||
})
|
})
|
||||||
)(APModifyPost);
|
)(APAddPost);
|
||||||
|
|||||||
Regular → Executable
+13
-8
@@ -1,8 +1,9 @@
|
|||||||
import React, { ChangeEvent, FormEvent } from "react";
|
import React, { ChangeEvent, FormEvent } from "react";
|
||||||
import Group from "../../../ui/Group/Group";
|
import Group from "../../../ui/Group/Group";
|
||||||
import NotFoundError from "../../../components/NotFoundError";
|
import NotFoundError from "../../../components/NotFound/NotFoundError";
|
||||||
import { deletePost, fetchPost, updatePost } from "../../../api/news";
|
import { deletePost, fetchPost, updatePost } from "../../../api/news";
|
||||||
import './APNews.sass';
|
import './APNews.sass';
|
||||||
|
import '../../../ui/TextField/TextField.sass'
|
||||||
import Input from "../../../ui/Input/Input";
|
import Input from "../../../ui/Input/Input";
|
||||||
import Button from "../../../ui/Button/Button";
|
import Button from "../../../ui/Button/Button";
|
||||||
import BBEditor from "../../../ui/BBEditor/BBEditor";
|
import BBEditor from "../../../ui/BBEditor/BBEditor";
|
||||||
@@ -14,16 +15,17 @@ import ConfirmPopout from "../../../ui/ConfirmPopout/ConfirmPopout";
|
|||||||
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
import { setModal } from "../../../store/ducks/modal";
|
import { setModal } from "../../../store/ducks/modal";
|
||||||
import { addNotification } from "../../../store/ducks/notifications";
|
import { addNotification } from "../../../store/ducks/notifications";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { id: number }, path: string }
|
match: { params: { id: number } }
|
||||||
history: { push: (path: string) => void }
|
history: { push: (path: string) => void }
|
||||||
setPopout: (modal: React.ReactNode | null) => void
|
setPopout: (modal: React.ReactNode | null) => void
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
post: PostType | null
|
post: Unmei.PostType | null
|
||||||
title: string
|
title: string
|
||||||
|
|
||||||
previewShort: string
|
previewShort: string
|
||||||
@@ -41,7 +43,7 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match } = this.props;
|
const { match } = this.props;
|
||||||
|
|
||||||
fetchPost(match.params.id).then((post: PostType) => {
|
fetchPost(match.params.id).then(post => {
|
||||||
this.setState({ post, title: post.title });
|
this.setState({ post, title: post.title });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -62,7 +64,7 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
<NotificationMessage level={"success"}>
|
<NotificationMessage level={"success"}>
|
||||||
Успешно!
|
Успешно!
|
||||||
</NotificationMessage>
|
</NotificationMessage>
|
||||||
);
|
)
|
||||||
addNotification(notification)
|
addNotification(notification)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,8 +97,7 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
|
|
||||||
deletePost = async (postId: number) => {
|
deletePost = async (postId: number) => {
|
||||||
await deletePost(postId);
|
await deletePost(postId);
|
||||||
const path = this.props.match.path.split('/')
|
this.props.history.push('/');
|
||||||
this.props.history.push(`/${path[1]}/${path[2]}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_deletePost = () => {
|
_deletePost = () => {
|
||||||
@@ -117,6 +118,10 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
if(!post) return <NotFoundError/>;
|
if(!post) return <NotFoundError/>;
|
||||||
return (
|
return (
|
||||||
<Group title={title}>
|
<Group title={title}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{title} / Админ-панель</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<form onSubmit={this.savePost} className={'APPost'}>
|
<form onSubmit={this.savePost} className={'APPost'}>
|
||||||
<Input type="text" placeholder={'Заголовок'} value={post.title} onChange={this.changeTitle}/>
|
<Input type="text" placeholder={'Заголовок'} value={post.title} onChange={this.changeTitle}/>
|
||||||
|
|
||||||
@@ -147,7 +152,7 @@ class APModifyPost extends React.Component<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Group>
|
</Group>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+7
-2
@@ -3,13 +3,14 @@ import { fetchNews } from "../../../api/news";
|
|||||||
import Group from "../../../ui/Group/Group";
|
import Group from "../../../ui/Group/Group";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Button from "../../../ui/Button/Button";
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { path: string }
|
match: { path: string }
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
news: PostType[]
|
news: Unmei.PostType[]
|
||||||
};
|
};
|
||||||
|
|
||||||
class APNews extends React.Component<Props, State> {
|
class APNews extends React.Component<Props, State> {
|
||||||
@@ -27,6 +28,10 @@ class APNews extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={'Новости'}>
|
<Group title={'Новости'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Новости</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<Link to={`${path}/new`}>
|
<Link to={`${path}/new`}>
|
||||||
<Button>Добавить новость</Button>
|
<Button>Добавить новость</Button>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -39,7 +44,7 @@ class APNews extends React.Component<Props, State> {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import React, { ChangeEvent, FormEvent } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
import './APNovel.sass';
|
||||||
|
|
||||||
|
import Input from "../../../ui/Input/Input";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import BBEditor from "../../../ui/BBEditor/BBEditor";
|
||||||
|
import Title from "../../../ui/Title/Title";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
|
|
||||||
|
import { createNovel } from "../../../api/novels";
|
||||||
|
import { fetchGenres } from '../../../api/genres'
|
||||||
|
|
||||||
|
import { setModal } from "../../../store/ducks/modal";
|
||||||
|
import { addNotification } from "../../../store/ducks/notifications";
|
||||||
|
// @ts-ignore
|
||||||
|
import parser from 'bbcode-to-react';
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
history: { push: (path: string) => void }
|
||||||
|
setPopout: (modal: React.ReactNode | null) => void
|
||||||
|
addNotification: (notification: React.ReactNode) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
novel: Unmei.NovelType
|
||||||
|
genres: Unmei.GenreType[]
|
||||||
|
|
||||||
|
previewDescription: string
|
||||||
|
};
|
||||||
|
|
||||||
|
class APAddNovel extends React.Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
novel: {} as Unmei.NovelType, genres: [],
|
||||||
|
previewDescription: '',
|
||||||
|
}
|
||||||
|
private description = React.createRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
|
saveNovel = async(event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const { novel } = this.state;
|
||||||
|
if(!this.description.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
novel.description = this.description.current.value;
|
||||||
|
const { addNotification } = this.props;
|
||||||
|
|
||||||
|
createNovel(novel).then(createdNovel => {
|
||||||
|
const notification = (
|
||||||
|
<NotificationMessage level={"success"}>
|
||||||
|
Успешно создана новелла с ID {createdNovel.id}!
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
addNotification(notification);
|
||||||
|
this.props.history.push('/');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
fetchGenres().then(genres => this.setState({ genres }));
|
||||||
|
}
|
||||||
|
|
||||||
|
previewDescription = () => {
|
||||||
|
if(!this.description.current) return;
|
||||||
|
this.setState({ previewDescription: parser.toReact(this.description.current.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
changeOriginalName = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { novel } = this.state;
|
||||||
|
novel.original_name = event.target.value;
|
||||||
|
this.setState({ novel: novel });
|
||||||
|
}
|
||||||
|
|
||||||
|
changeLocalizedName = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { novel } = this.state;
|
||||||
|
novel.localized_name = event.target.value;
|
||||||
|
this.setState({ novel });
|
||||||
|
}
|
||||||
|
|
||||||
|
changeGenres = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selected = Array.from(e.target.selectedOptions);
|
||||||
|
console.log(selected.forEach(el => el.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const novel = Object.assign({}, this.state.novel);
|
||||||
|
const target = event.target;
|
||||||
|
// @ts-ignore
|
||||||
|
novel[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||||
|
this.setState({ novel });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { novel, genres, previewDescription } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group title={'Новая новелла'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{novel.original_name ? `${novel.original_name} / Админ-панель` : 'Новая новелла'}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<form onSubmit={this.saveNovel} className='APNovel'>
|
||||||
|
<Input type="text" placeholder={'Оригинальное название'} value={novel.original_name} onChange={this.changeOriginalName}/>
|
||||||
|
<Input type="text" placeholder={'Имя на русском'} value={novel.localized_name} onChange={this.changeLocalizedName}/>
|
||||||
|
<label>Демо? <Input type="checkbox" checked={novel.is_demo} onChange={this.onChange} name='is_demo'/></label>
|
||||||
|
|
||||||
|
<label>Жанры:
|
||||||
|
<select multiple onChange={this.changeGenres}>
|
||||||
|
{genres.map(genre => (
|
||||||
|
<option value={genre.id}>{genre.localized_name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className={'APNovel__Description'}>
|
||||||
|
<Title>Описание</Title>
|
||||||
|
<BBEditor inputRef={this.description} placeholder={'Описание'}/>
|
||||||
|
{previewDescription.length > 0 && (<>
|
||||||
|
<Title>Предпросмотр</Title>
|
||||||
|
<div>{previewDescription}</div>
|
||||||
|
</>)}
|
||||||
|
<Button type={"button"} onClick={this.previewDescription}>Предпросмотр</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button style={{ marginRight: '1rem' }}>Сохранить</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null,
|
||||||
|
dispatch => ({
|
||||||
|
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
||||||
|
})
|
||||||
|
)(APAddNovel);
|
||||||
Regular → Executable
+125
-90
@@ -1,132 +1,164 @@
|
|||||||
import React, { ChangeEvent, FormEvent } from "react";
|
import React, { ChangeEvent, FormEvent } from "react";
|
||||||
import Group from "../../../ui/Group/Group";
|
|
||||||
import NotFoundError from "../../../components/NotFoundError";
|
|
||||||
import './APNovels.sass';
|
|
||||||
import Input from "../../../ui/Input/Input";
|
|
||||||
import Button from "../../../ui/Button/Button";
|
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { setModal } from "../../../store/ducks/modal";
|
|
||||||
|
import './APNovel.sass';
|
||||||
|
|
||||||
|
import Loading from "../../../components/Loading";
|
||||||
|
|
||||||
import { addNotification } from "../../../store/ducks/notifications";
|
import { addNotification } from "../../../store/ducks/notifications";
|
||||||
import { fetchGenres, fetchNovel, updateNovel } from "../../../api/novels";
|
import { setPreloader } from "../../../store/ducks/preloader";
|
||||||
|
|
||||||
|
import { fetchNovel, updateNovel, uploadNovelCover } from "../../../api/novels";
|
||||||
|
|
||||||
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import Title from "../../../ui/Title/Title";
|
||||||
|
import BBEditor from "../../../ui/BBEditor/BBEditor";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import Input from "../../../ui/Input/Input";
|
||||||
|
import Select from "../../../ui/Select/Select";
|
||||||
|
import Option from "../../../ui/Select/Option";
|
||||||
|
|
||||||
|
import { fetchGenres } from "../../../api/genres";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
import TextField from "../../../ui/TextField/TextField";
|
import { Helmet } from "react-helmet";
|
||||||
import Select from "../../../ui/Select/Select";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { id: number }, path: string }
|
match: { params: { id: number } }
|
||||||
history: { push: (path: string) => void }
|
|
||||||
setPopout: (modal: React.ReactNode | null) => void
|
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
novel: NovelType | null
|
novel: Unmei.NovelType | null
|
||||||
genres: GenreType[]
|
genres: Unmei.GenreType[]
|
||||||
title: string
|
cover: File | null
|
||||||
|
originalName: string
|
||||||
|
previewDescription: string
|
||||||
};
|
};
|
||||||
|
|
||||||
class APModifyNovel extends React.Component<Props, State> {
|
class APModifyNovel extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
novel: null, genres: [],
|
novel: null, originalName: '',
|
||||||
title: ''
|
genres: [], cover: null,
|
||||||
|
previewDescription: ''
|
||||||
}
|
}
|
||||||
|
private description = React.createRef<HTMLTextAreaElement>();
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match } = this.props;
|
const { match: { params: { id } }, setPreloader } = this.props;
|
||||||
|
|
||||||
fetchNovel(match.params.id).then(novel => {
|
setPreloader(true);
|
||||||
|
fetchNovel(id).then(async novel => {
|
||||||
novel.release_date = new Date(novel.release_date);
|
novel.release_date = new Date(novel.release_date);
|
||||||
this.setState({ novel, title: novel.original_name });
|
const genres = await fetchGenres();
|
||||||
|
this.setState({ novel, genres, originalName: novel.original_name });
|
||||||
|
setPreloader(false);
|
||||||
});
|
});
|
||||||
fetchGenres().then(genres => this.setState({ genres }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeOriginalTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
onChange = (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
const { novel } = this.state;
|
const novel = Object.assign({}, this.state.novel);
|
||||||
if(!novel) return;
|
const target = event.target;
|
||||||
|
// @ts-ignore
|
||||||
const newTitle = e.target.value;
|
novel[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||||
document.title = `${newTitle} / Админ-панель`;
|
if(target.type === 'date')
|
||||||
novel.original_name = newTitle;
|
novel.release_date = new Date(novel.release_date);
|
||||||
this.setState({ novel });
|
this.setState({ novel });
|
||||||
}
|
}
|
||||||
|
|
||||||
changeLocalizedTitle = (e: ChangeEvent<HTMLInputElement>) => {
|
onChangeCover = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const { novel } = this.state;
|
const files = event.target.files;
|
||||||
if(!novel) return;
|
if(!files) return;
|
||||||
|
this.setState({ cover: files[0] });
|
||||||
|
}
|
||||||
|
|
||||||
novel.localized_name = e.target.value;
|
saveCover = () => {
|
||||||
|
const { novel, cover } = this.state;
|
||||||
|
if(cover && novel) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('cover', cover);
|
||||||
|
uploadNovelCover(novel.id, formData).then(console.log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const { novel } = this.state;
|
||||||
|
const { addNotification } = this.props;
|
||||||
|
|
||||||
|
if(novel === null) {
|
||||||
|
const notification = (
|
||||||
|
<NotificationMessage level={"error"}>
|
||||||
|
Новелла - null!
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
addNotification(notification);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateNovel(novel).then(() => {
|
||||||
|
this.saveCover();
|
||||||
|
const notification = (
|
||||||
|
<NotificationMessage level={"success"}>
|
||||||
|
Новелла успешно изменена
|
||||||
|
</NotificationMessage>
|
||||||
|
);
|
||||||
|
addNotification(notification)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeGenres = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selected = Array.from(e.target.selectedOptions);
|
||||||
|
const { novel, genres } = this.state;
|
||||||
|
if(!novel) return;
|
||||||
|
const selectedGenres = selected.map(el => genres.filter(genre => genre.id === parseInt(el.value))[0]);
|
||||||
|
novel.genres = selectedGenres;
|
||||||
this.setState({ novel });
|
this.setState({ novel });
|
||||||
}
|
}
|
||||||
|
|
||||||
changeGenres = (e: ChangeEvent<HTMLSelectElement>) => {
|
previewDescription = () => {
|
||||||
const { novel } = this.state;
|
if(!this.description.current) return;
|
||||||
if(!novel) return;
|
this.setState({ previewDescription: parser.toReact(this.description.current.value) });
|
||||||
|
|
||||||
novel.genres = Array.from(e.target.selectedOptions).map(genre => {
|
|
||||||
return this.state.genres.filter(g => g.name === genre.value)[0];
|
|
||||||
});
|
|
||||||
this.setState({ novel });
|
|
||||||
}
|
|
||||||
|
|
||||||
saveNovel = (e: FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const { novel } = this.state;
|
|
||||||
if(!novel) return;
|
|
||||||
updateNovel(novel).catch(err => {
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { novel, genres, title } = this.state;
|
const { novel, genres, originalName, previewDescription } = this.state;
|
||||||
|
|
||||||
if(!novel) return <NotFoundError/>;
|
if(!novel) return <Loading/>;
|
||||||
return (
|
return (
|
||||||
<Group title={title}>
|
<Group title={originalName}>
|
||||||
<form className={'APNovel'} onSubmit={this.saveNovel}>
|
<Helmet>
|
||||||
<Input type="text" className={'APNovel_Input'} placeholder={'Оригинальное название'} value={novel.original_name} onChange={this.changeOriginalTitle}/>
|
<title>{novel.original_name} / Админ-панель</title>
|
||||||
<Input type="text" className={'APNovel_Input'} placeholder={'Название на русском'} value={novel.localized_name} onChange={this.changeLocalizedTitle}/>
|
</Helmet>
|
||||||
|
|
||||||
<TextField className={'APNovel_TextField'} placeholder={'Описание'} style={{ height: 100 }} value={novel.description} onChange={(e) => {
|
<form className={'APNovel'} onSubmit={this.onSubmit}>
|
||||||
novel.description = e.target.value;
|
<Input type="text" onChange={this.onChange} name={'original_name'} value={novel.original_name}/>
|
||||||
this.setState({ novel });
|
<Input type='text' onChange={this.onChange} name={'localized_name'} value={novel.localized_name}/>
|
||||||
}}/>
|
<label>Демо? <Input type="checkbox" checked={novel.is_demo} onChange={this.onChange} name='is_demo'/></label>
|
||||||
|
<label><Input type="file" onChange={this.onChangeCover}/></label>
|
||||||
|
<label>Дата выхода: <Input type="date" name='release_date' value={novel.release_date.toISOString().substring(0, 10)} onChange={this.onChange}/></label>
|
||||||
|
|
||||||
<label style={{ display: 'flex', alignItems: "center" }}>
|
<label>Жанры:
|
||||||
<span style={{ marginRight: 8 }}>Жанры:</span>
|
<Select multiple onChange={this.changeGenres} value={novel.genres.map(g => g.id.toString())}>
|
||||||
<Select
|
|
||||||
multiple
|
|
||||||
onChange={this.changeGenres}
|
|
||||||
value={novel.genres.map(genre => genre.name)}
|
|
||||||
>
|
|
||||||
{genres.map(genre => (
|
{genres.map(genre => (
|
||||||
<option
|
<Option value={genre.id} key={genre.id}>{genre.localized_name}</Option>
|
||||||
key={genre.id}
|
|
||||||
value={genre.name}
|
|
||||||
>
|
|
||||||
{genre.localized_name}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label>
|
<div className={'APNovel_Description'}>
|
||||||
<span style={{ marginRight: 8 }}>Дата выхода:</span>
|
<Title>Описание</Title>
|
||||||
<input
|
<BBEditor inputRef={this.description} value={novel.description} name={'description'} onChange={this.onChange} placeholder={'Описание'}/>
|
||||||
type="date"
|
{previewDescription.length > 0 && (<>
|
||||||
value={novel.release_date.toISOString().substring(0, 10)}
|
<Title>Предпросмотр</Title>
|
||||||
onChange={e => {
|
<div>{previewDescription}</div>
|
||||||
novel.release_date = new Date(e.target.value);
|
</>)}
|
||||||
this.setState({ novel });
|
<Button type={"button"} onClick={this.previewDescription}>Предпросмотр</Button>
|
||||||
}}
|
</div>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div>
|
<div style={{ textAlign: "right" }}>
|
||||||
<Button style={{ marginRight: '1rem' }} className={'APNovel_Button'}>Сохранить</Button>
|
<Button>Сохранить!</Button>
|
||||||
<Button style={{ color: 'var(--error-bg-color)' }} className={'APNovel_Button'} type={"button"}>Удалить</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -134,9 +166,12 @@ class APModifyNovel extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(null,
|
export default connect(
|
||||||
dispatch => ({
|
(state: StoreState) => ({
|
||||||
setPopout: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
user: state.currentUser
|
||||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
}),
|
||||||
|
(dispatch) => ({
|
||||||
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
|
||||||
|
setPreloader: (status: boolean) => dispatch(setPreloader(status))
|
||||||
})
|
})
|
||||||
)(APModifyNovel);
|
)(APModifyNovel);
|
||||||
|
|||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
.APNovel
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
.Input, .TextField, .Button
|
||||||
|
font-size: 1.2rem
|
||||||
|
|
||||||
|
.Input, .TextField
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
.TextField
|
||||||
|
width: 100%
|
||||||
|
min-height: 4rem
|
||||||
|
resize: none
|
||||||
|
|
||||||
|
&_Title
|
||||||
|
text-align: center
|
||||||
|
font-weight: 600
|
||||||
|
font-size: 1.5rem
|
||||||
|
|
||||||
|
&_Description
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
align-items: flex-start
|
||||||
|
margin-bottom: 1rem
|
||||||
|
|
||||||
|
.TextField
|
||||||
|
height: 10rem
|
||||||
|
resize: vertical
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
.APNovel
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
|
|
||||||
&_Input, &_Button, &_TextField
|
|
||||||
font-size: 1.2rem
|
|
||||||
margin: .5rem 0
|
|
||||||
|
|
||||||
&_TextField
|
|
||||||
resize: vertical
|
|
||||||
Regular → Executable
+15
-9
@@ -1,16 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Button from "../../../ui/Button/Button";
|
|
||||||
import Group from "../../../ui/Group/Group";
|
|
||||||
import { fetchNovels } from "../../../api/novels";
|
import { fetchNovels } from "../../../api/novels";
|
||||||
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { path: string }
|
match: { path: string }
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
novels: NovelType[]
|
novels: Unmei.NovelType[]
|
||||||
};
|
};
|
||||||
|
|
||||||
class APNovels extends React.Component<Props, State> {
|
class APNovels extends React.Component<Props, State> {
|
||||||
@@ -23,25 +24,30 @@ class APNovels extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { match: { path } } = this.props;
|
|
||||||
const { novels } = this.state;
|
const { novels } = this.state;
|
||||||
|
const { match: { path } } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={'Новелы'}>
|
<Group title={'Новеллы'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Новеллы</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<Link to={`${path}/new`}>
|
<Link to={`${path}/new`}>
|
||||||
<Button>Добавить новелу</Button>
|
<Button>Добавить новеллу</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{novels.sort((a, b) => a.id-b.id).map(novel => (
|
{novels.sort((a, b) => a.id - b.id).map(novel => (
|
||||||
<Link to={`${path}/${novel.id}`} key={novel.id}>
|
<Link to={`${path}/${novel.id}`} key={novel.id}>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{novel.id}</div>
|
<div style={{ marginRight: '.5rem', fontWeight: 600 }}>{novel.id}</div>
|
||||||
<div>{novel.original_name}</div>
|
<div style={{ marginRight: '2rem' }}>{novel.original_name}</div>
|
||||||
|
<div>{novel.localized_name}</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</Group>
|
</Group>
|
||||||
)
|
)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|||||||
Regular → Executable
+14
-5
@@ -1,10 +1,15 @@
|
|||||||
import React, { ChangeEvent, FormEvent } from "react";
|
import React, { ChangeEvent, FormEvent } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { fetchUser, updateUser } from "../../../api/users";
|
import { Helmet } from "react-helmet";
|
||||||
import Loading from "../../../components/Loading";
|
|
||||||
import './APUser.sass';
|
import './APUser.sass';
|
||||||
|
|
||||||
|
import { fetchUser, updateUser } from "../../../api/users";
|
||||||
|
|
||||||
|
import Loading from "../../../components/Loading";
|
||||||
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
import NotificationMessage from "../../../ui/Notifications/NotificationMessage";
|
||||||
import Button from "../../../ui/Button/Button";
|
import Button from "../../../ui/Button/Button";
|
||||||
|
|
||||||
import { addNotification } from "../../../store/ducks/notifications";
|
import { addNotification } from "../../../store/ducks/notifications";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -13,7 +18,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
user: UserType | null
|
user: Unmei.UserType | null
|
||||||
username: string
|
username: string
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,7 +29,7 @@ class APModifyUser extends React.Component<Props, State> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match: { params: { id } } } = this.props;
|
const { match: { params: { id } } } = this.props;
|
||||||
fetchUser(id).then((user: UserType) => {
|
fetchUser(id).then((user: Unmei.UserType) => {
|
||||||
this.setState({ user, username: user.username });
|
this.setState({ user, username: user.username });
|
||||||
document.title = `${user.username} / Админ-панель`;
|
document.title = `${user.username} / Админ-панель`;
|
||||||
})
|
})
|
||||||
@@ -35,7 +40,7 @@ class APModifyUser extends React.Component<Props, State> {
|
|||||||
const target = event.target;
|
const target = event.target;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
user[target.name] = target.type === 'checkbox' ? target.checked : target.value;
|
||||||
this.setState({ user })
|
this.setState({ user });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = (event: FormEvent) => {
|
onSubmit = (event: FormEvent) => {
|
||||||
@@ -63,6 +68,10 @@ class APModifyUser extends React.Component<Props, State> {
|
|||||||
if(!user) return <Loading/>;
|
if(!user) return <Loading/>;
|
||||||
return (
|
return (
|
||||||
<div className={'APUser__Modify'}>
|
<div className={'APUser__Modify'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{user.username} / Админ-панель</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className={'APUser__Modify_Title'}>{username}</div>
|
<div className={'APUser__Modify_Title'}>{username}</div>
|
||||||
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
<form className={'APUser__Modify_Form'} onSubmit={this.onSubmit}>
|
||||||
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'}
|
<label>Имя пользователя: <input type="text" onChange={this.onChange} name={'username'}
|
||||||
|
|||||||
Regular → Executable
+1
-1
@@ -24,4 +24,4 @@
|
|||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
&[type='checkbox']
|
&[type='checkbox']
|
||||||
width: 16px
|
width: 0
|
||||||
|
|||||||
Regular → Executable
+6
-1
@@ -3,13 +3,14 @@ import { connect } from "react-redux";
|
|||||||
import { fetchUsers } from "../../../api/users";
|
import { fetchUsers } from "../../../api/users";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Group from "../../../ui/Group/Group";
|
import Group from "../../../ui/Group/Group";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { path: string }
|
match: { path: string }
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
users: UserType[]
|
users: Unmei.UserType[]
|
||||||
};
|
};
|
||||||
|
|
||||||
class APUsers extends React.Component<Props, State> {
|
class APUsers extends React.Component<Props, State> {
|
||||||
@@ -27,6 +28,10 @@ class APUsers extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={'Пользователи'}>
|
<Group title={'Пользователи'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Пользователи</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
{users.sort((a, b) => a.id - b.id).map(user => (
|
{users.sort((a, b) => a.id - b.id).map(user => (
|
||||||
<Link to={`${path}/${user.id}`} key={user.id}>
|
<Link to={`${path}/${user.id}`} key={user.id}>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+28
-13
@@ -4,16 +4,19 @@ import './Character.sass'
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
import NovelItem from "../../ui/NovelItem/NovelItem";
|
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||||
import Loading from "../../components/Loading";
|
import NotFoundError from "../../components/NotFound/NotFoundError";
|
||||||
import NotFoundError from "../../components/NotFoundError";
|
import { connect } from "react-redux";
|
||||||
|
import { setPreloader } from "../../store/ducks/preloader";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { charId: number } }
|
match: { params: { charId: number } }
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
char: CharacterType | null
|
char: Unmei.CharacterType | null
|
||||||
novels: NovelType[]
|
novels: Unmei.NovelType[]
|
||||||
errorCode: number
|
errorCode: number
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,27 +26,34 @@ class Character extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
const { charId } = this.props.match.params;
|
const { setPreloader, match: { params: { charId } } } = this.props;
|
||||||
fetchChar(charId).then(char => {
|
|
||||||
this.setState({ char });
|
setPreloader(true);
|
||||||
|
fetchChar(charId).then(async char => {
|
||||||
document.title = char.localized_name ? `${char.original_name} / ${char.localized_name}` : char.original_name;
|
document.title = char.localized_name ? `${char.original_name} / ${char.localized_name}` : char.original_name;
|
||||||
|
|
||||||
|
const novels = await fetchCharNovels(charId);
|
||||||
|
this.setState({ novels, char });
|
||||||
|
setPreloader(false);
|
||||||
}).catch(r => this.setState({ errorCode: r.code }));
|
}).catch(r => this.setState({ errorCode: r.code }));
|
||||||
fetchCharNovels(charId).then(novels => this.setState({ novels }));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { char, novels, errorCode } = this.state;
|
const { char, novels, errorCode } = this.state;
|
||||||
if(errorCode === 100) return <NotFoundError/>;
|
if(errorCode === 100) return <NotFoundError/>;
|
||||||
if(!char) return <Loading/>;
|
if(!char) return null;
|
||||||
return (
|
return (
|
||||||
<div className={'Character'}>
|
<div className={'Character'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{char.localized_name && char.localized_name !== char.original_name ? `${char.original_name} / ${char.localized_name}` : char.original_name}</title>
|
||||||
|
<meta name='description' content={char.description}/>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className="Character__Main">
|
<div className="Character__Main">
|
||||||
<img src={char.image} alt={char.original_name} className="Character__Image" />
|
<img src={char.image} alt={char.original_name} className="Character__Image" />
|
||||||
<div className="Character__Info">
|
<div className="Character__Info">
|
||||||
<div className="Character__Info_Name">
|
<div className="Character__Info_Name">
|
||||||
{char.original_name !== char.localized_name ? (
|
{char.original_name !== char.localized_name ? `${char.original_name} / ${char.localized_name}` : char.original_name}
|
||||||
`${char.original_name} / ${char.localized_name}`
|
|
||||||
) : char.original_name}
|
|
||||||
</div>
|
</div>
|
||||||
<pre className="Character__Info_Description">{parser.toReact(char.description)}</pre>
|
<pre className="Character__Info_Description">{parser.toReact(char.description)}</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,4 +67,9 @@ class Character extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Character;
|
export default connect(
|
||||||
|
null,
|
||||||
|
dispatch => ({
|
||||||
|
setPreloader: (s: boolean) => dispatch(setPreloader(s))
|
||||||
|
})
|
||||||
|
)(Character);
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import React from "react";
|
|||||||
import { getClub } from "../../api/clubs";
|
import { getClub } from "../../api/clubs";
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import Group from "../../ui/Group/Group";
|
import Group from "../../ui/Group/Group";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { clubId: number } }
|
match: { params: { clubId: number } }
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
club: ClubType | null
|
club: Unmei.ClubType | null
|
||||||
}
|
}
|
||||||
|
|
||||||
class Club extends React.Component<Props, State> {
|
class Club extends React.Component<Props, State> {
|
||||||
@@ -30,6 +31,10 @@ class Club extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={club.name}>
|
<Group title={club.name}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{club.name}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<img src={club.avatar} width={96} height={96} style={{ borderRadius: '50%' }} alt=""/>
|
<img src={club.avatar} width={96} height={96} style={{ borderRadius: '50%' }} alt=""/>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
|
|||||||
|
|
||||||
type Props = {}
|
type Props = {}
|
||||||
type State = {
|
type State = {
|
||||||
clubs: ClubType[]
|
clubs: Unmei.ClubType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Clubs extends React.Component<Props, State> {
|
class Clubs extends React.Component<Props, State> {
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+25
-16
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
faBold,
|
faBold,
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
faTint,
|
faTint,
|
||||||
faUnderline
|
faUnderline
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import Comment from '../../ui/Comment/Comment';
|
|
||||||
import './Comments.sass';
|
import './Comments.sass';
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import { fetchUser } from "../../api/users";
|
import { fetchUser } from "../../api/users";
|
||||||
@@ -17,10 +16,13 @@ import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
|||||||
import Button from "../../ui/Button/Button";
|
import Button from "../../ui/Button/Button";
|
||||||
import { addNotification } from "../../store/ducks/notifications";
|
import { addNotification } from "../../store/ducks/notifications";
|
||||||
|
|
||||||
|
const Comment = React.lazy(() => import('../../ui/Comment/Comment'));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comments: CommentType[]
|
comments: Unmei.CommentType[]
|
||||||
|
commentsLoaded: boolean
|
||||||
hasMore: boolean
|
hasMore: boolean
|
||||||
user: UserType
|
user: Unmei.UserType
|
||||||
loadMore: () => void
|
loadMore: () => void
|
||||||
sendComment: (text: string) => void
|
sendComment: (text: string) => void
|
||||||
addNotification: (notification: React.ReactNode) => void
|
addNotification: (notification: React.ReactNode) => void
|
||||||
@@ -28,7 +30,7 @@ type Props = {
|
|||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
commentText: string
|
commentText: string
|
||||||
users: Map<number, UserType>
|
users: Map<number, Unmei.UserType>
|
||||||
};
|
};
|
||||||
|
|
||||||
class Comments extends React.Component<Props, State> {
|
class Comments extends React.Component<Props, State> {
|
||||||
@@ -85,8 +87,18 @@ class Comments extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
const { comments } = this.props;
|
||||||
|
let { users } = this.state;
|
||||||
|
|
||||||
|
for(let comment of comments)
|
||||||
|
if(!users.has(comment.user_id))
|
||||||
|
users = users.set(comment.user_id, await fetchUser(comment.user_id));
|
||||||
|
this.setState({ users });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { user, comments, hasMore, loadMore } = this.props;
|
const { user, comments, commentsLoaded, hasMore, loadMore } = this.props;
|
||||||
const { users, commentText } = this.state;
|
const { users, commentText } = this.state;
|
||||||
if(!user)
|
if(!user)
|
||||||
return <Loading/>;
|
return <Loading/>;
|
||||||
@@ -115,23 +127,20 @@ class Comments extends React.Component<Props, State> {
|
|||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
<Button type="submit" className={'Comments_Submit'}>Написать</Button>
|
<Button type="submit" className={'Comments_Submit'}>Написать</Button>
|
||||||
<div style={{ marginLeft: '.5rem', color: '#bb3333' }}>Комментарии в разработке!</div>
|
|
||||||
</div>
|
</div>
|
||||||
{hasMore && <Button onClick={loadMore}>Загрузить ещё</Button>}
|
{hasMore && <Button onClick={loadMore} role={'button'}>Загрузить ещё</Button>}
|
||||||
</div>
|
</div>
|
||||||
</form>}
|
</form>}
|
||||||
{comments.length === 0 && (
|
{!commentsLoaded && <Loading/>}
|
||||||
<Loading/>
|
{comments.length > 0 ? (<Suspense fallback={<Loading/>}>
|
||||||
)}
|
{comments.map(comment => (
|
||||||
{comments.length > 0 ? (
|
|
||||||
comments.map(comment => (
|
|
||||||
<Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)}/>
|
<Comment text={comment.text} key={comment.id} user={users.get(comment.user_id)}/>
|
||||||
))
|
))}
|
||||||
) : (
|
</Suspense>) : (
|
||||||
'Никто не оставил комментарий к этой новелле... :c'
|
'Никто не оставил комментарий к этой новелле... :c'
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+12
-3
@@ -1,12 +1,21 @@
|
|||||||
import React from "react";
|
import React, { lazy, Suspense } from "react";
|
||||||
import News from "./News";
|
|
||||||
import './Main.sass';
|
import './Main.sass';
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import Loading from "../../ui/Loading";
|
||||||
|
|
||||||
|
const News = lazy(() => import('./News'));
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={'Main'}>
|
<div className={'Main'}>
|
||||||
<News/>
|
<Helmet>
|
||||||
|
<title>Unmei</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Suspense fallback={<Loading/>}>
|
||||||
|
<News/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Regular → Executable
+6
-4
@@ -1,11 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { fetchNews } from "../../api/news";
|
|
||||||
import Loading from "../../components/Loading";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
import NewsPost from "../../ui/NewsPost/NewsPost";
|
import NewsPost from "../../ui/NewsPost/NewsPost";
|
||||||
|
|
||||||
|
import { fetchNews } from "../../api/news";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
news: PostType[]
|
news: Unmei.PostType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class News extends React.Component<{}, State> {
|
class News extends React.Component<{}, State> {
|
||||||
@@ -14,7 +16,7 @@ class News extends React.Component<{}, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
fetchNews().then((news: PostType[]) => {
|
fetchNews().then(news => {
|
||||||
news.forEach(p => p.date = new Date(p.date));
|
news.forEach(p => p.date = new Date(p.date));
|
||||||
this.setState({ news })
|
this.setState({ news })
|
||||||
});
|
});
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
+5
-5
@@ -57,9 +57,9 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
document.body.setAttribute('theme', settings.theme);
|
document.body.setAttribute('theme', settings.theme);
|
||||||
setSettings(settings);
|
setSettings(settings);
|
||||||
});
|
});
|
||||||
}).catch((r: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
this.setState({ error: errors[r.code] });
|
this.setState({ error: errors[err.code] });
|
||||||
if(r.code === 9) this.setState({ recaptchaNeeded: true });
|
if(err.code === 9) this.setState({ recaptchaNeeded: true });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -110,8 +110,8 @@ class LoginModal extends React.Component<Props, State> {
|
|||||||
|
|
||||||
export default connect(null,
|
export default connect(null,
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (user: UserType) => dispatch(setUser(user)),
|
setUser: (user: Unmei.UserType) => dispatch(setUser(user)),
|
||||||
setSettings: (settings: UserSettingsType) => dispatch(setSettings(settings)),
|
setSettings: (settings: Unmei.UserSettingsType) => dispatch(setSettings(settings)),
|
||||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
|
||||||
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
hideModal: () => dispatch(hideModal())
|
hideModal: () => dispatch(hideModal())
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
+17
-10
@@ -1,11 +1,12 @@
|
|||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import { fetchNews } from "../../api/news";
|
import { fetchNews } from "../../api/news";
|
||||||
import NewsPost from "../../ui/NewsPost/NewsPost";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
const NewsPost = React.lazy(() => import('../../ui/NewsPost/NewsPost'));
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
news: PostType[]
|
news: Unmei.PostType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class AllNews extends React.Component<{}, State> {
|
class AllNews extends React.Component<{}, State> {
|
||||||
@@ -15,7 +16,7 @@ class AllNews extends React.Component<{}, State> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.title = 'Новости';
|
document.title = 'Новости';
|
||||||
fetchNews().then((news: PostType[]) => {
|
fetchNews().then(news => {
|
||||||
news.forEach(p => p.date = new Date(p.date));
|
news.forEach(p => p.date = new Date(p.date));
|
||||||
this.setState({ news })
|
this.setState({ news })
|
||||||
});
|
});
|
||||||
@@ -27,15 +28,21 @@ class AllNews extends React.Component<{}, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="News">
|
<div className="News">
|
||||||
|
<Helmet>
|
||||||
|
<title>Новости</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className="News__Title">
|
<div className="News__Title">
|
||||||
Новости
|
Новости
|
||||||
</div>
|
</div>
|
||||||
{/*{<RoundProgressBar progress={50} alternative color={'red'}/>}*/}
|
|
||||||
{news.map(post => (
|
<Suspense fallback={<Loading/>}>
|
||||||
<Link key={post.id} to={`/post/${post.id}`}>
|
{news.map(post => (
|
||||||
<NewsPost {...post} />
|
<Link key={post.id} to={`/news/${post.id}`}>
|
||||||
</Link>
|
<NewsPost {...post} />
|
||||||
))}
|
</Link>
|
||||||
|
))}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+26
-7
@@ -1,18 +1,22 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { fetchPost } from "../../api/news";
|
import { fetchPost } from "../../api/news";
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import NotFoundError from "../../components/NotFoundError";
|
import NotFoundError from "../../components/NotFound/NotFoundError";
|
||||||
import './Post.sass'
|
import './Post.sass'
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import parser from 'bbcode-to-react';
|
import parser from 'bbcode-to-react';
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setPreloader } from "../../store/ducks/preloader";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { postId: number } }
|
match: { params: { postId: number } }
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
post: PostType | null
|
post: Unmei.PostType | null
|
||||||
code: number
|
code: number
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -22,13 +26,15 @@ class Post extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { match: { params: { postId } } } = this.props;
|
const { match: { params: { postId } }, setPreloader } = this.props;
|
||||||
|
|
||||||
fetchPost(postId).then((post: PostType) => {
|
setPreloader(true);
|
||||||
|
fetchPost(postId).then(post => {
|
||||||
post.date = new Date(post.date);
|
post.date = new Date(post.date);
|
||||||
this.setState({ post });
|
this.setState({ post });
|
||||||
}).catch((error: ApiError) => {
|
setPreloader(false);
|
||||||
this.setState({ code: error.code })
|
}).catch((err: Unmei.ApiError) => {
|
||||||
|
this.setState({ code: err.code })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +45,14 @@ class Post extends React.Component<Props, State> {
|
|||||||
if(!post) return <Loading/>;
|
if(!post) return <Loading/>;
|
||||||
return (
|
return (
|
||||||
<div className={'Post'}>
|
<div className={'Post'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{post.title}</title>
|
||||||
|
<meta name='description' content={post.short_post}/>
|
||||||
|
|
||||||
|
<meta property="og:title" content={post.title} />
|
||||||
|
<meta property="og:description" content={post.short_post} />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className="Post__Title">{post.title}</div>
|
<div className="Post__Title">{post.title}</div>
|
||||||
<pre className='Post__Content'>{parser.toReact(post.short_post)}</pre>
|
<pre className='Post__Content'>{parser.toReact(post.short_post)}</pre>
|
||||||
<div className="Post__Footer">
|
<div className="Post__Footer">
|
||||||
@@ -52,4 +66,9 @@ class Post extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Post;
|
export default connect(
|
||||||
|
null,
|
||||||
|
dispatch => ({
|
||||||
|
setPreloader: (s: boolean) => dispatch(setPreloader(s))
|
||||||
|
})
|
||||||
|
)(Post);
|
||||||
|
|||||||
Regular → Executable
+60
-19
@@ -1,5 +1,15 @@
|
|||||||
@import "../../Constants"
|
@import "../../Constants"
|
||||||
|
|
||||||
|
@keyframes growDown
|
||||||
|
0%
|
||||||
|
transform: scaleY(0)
|
||||||
|
|
||||||
|
80%
|
||||||
|
transform: scaleY(1.1)
|
||||||
|
|
||||||
|
100%
|
||||||
|
transform: scaleY(1)
|
||||||
|
|
||||||
.Novel
|
.Novel
|
||||||
.Novel__Main
|
.Novel__Main
|
||||||
display: flex
|
display: flex
|
||||||
@@ -21,7 +31,7 @@
|
|||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
|
|
||||||
.Novel__Main_Status
|
.Novel__Main_Status
|
||||||
margin-bottom: 1rem
|
max-width: 350px
|
||||||
|
|
||||||
&_Mark
|
&_Mark
|
||||||
margin-top: .5rem
|
margin-top: .5rem
|
||||||
@@ -53,6 +63,9 @@
|
|||||||
&_Extended
|
&_Extended
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
font-size: 1rem
|
font-size: 1rem
|
||||||
|
overflow: hidden
|
||||||
|
animation: growDown 300ms ease-in-out forwards
|
||||||
|
transform-origin: top center
|
||||||
|
|
||||||
.Novel__Main_Status_Element
|
.Novel__Main_Status_Element
|
||||||
padding: .25rem
|
padding: .25rem
|
||||||
@@ -61,32 +74,60 @@
|
|||||||
.Novel__Main_Status_Element
|
.Novel__Main_Status_Element
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
|
||||||
.Novel__Info_Title
|
&_Info
|
||||||
@media (max-width: 600px)
|
width: 100%
|
||||||
margin-top: 1rem
|
|
||||||
|
|
||||||
font-size: 1.7rem
|
&_Title
|
||||||
font-weight: 600
|
@media (max-width: 600px)
|
||||||
margin-bottom: 1rem
|
margin-top: 1rem
|
||||||
|
|
||||||
.Novel__Info_Description
|
font-size: 1.7rem
|
||||||
margin-top: .5rem
|
font-weight: 600
|
||||||
@media (min-width: 600px)
|
margin-bottom: 4px
|
||||||
margin-left: 1rem
|
display: flex
|
||||||
font-size: 1rem
|
align-items: center
|
||||||
|
|
||||||
.Novel__Info_Genres
|
&_Demo
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
font-size: 1rem
|
|
||||||
|
|
||||||
.Genre
|
|
||||||
margin: .2rem
|
margin: .2rem
|
||||||
border: #992222 1px solid
|
border: var(--accent-color) 1px solid
|
||||||
padding: .1rem
|
padding: .1rem
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
font-weight: 300
|
font-weight: 300
|
||||||
|
|
||||||
|
&_Description
|
||||||
|
margin-top: 8px
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
&_Genres
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
font-size: 1rem
|
||||||
|
|
||||||
|
.Genre
|
||||||
|
margin: 2px
|
||||||
|
border: #992222 1px solid
|
||||||
|
padding: .1rem
|
||||||
|
border-radius: 3px
|
||||||
|
font-weight: 300
|
||||||
|
box-sizing: border-box
|
||||||
|
|
||||||
|
&_Rating
|
||||||
|
|
||||||
|
&_All
|
||||||
|
width: 250px
|
||||||
|
|
||||||
|
@media (max-width: 600px)
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
&_Mark
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
padding: 2px 4px
|
||||||
|
margin: 4px 0
|
||||||
|
box-sizing: border-box
|
||||||
|
background-color: var(--block-color)
|
||||||
|
border-radius: 2px
|
||||||
|
|
||||||
.Novel__Comments
|
.Novel__Comments
|
||||||
margin-top: 1rem
|
margin-top: 1rem
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
|
|||||||
Regular → Executable
+132
-94
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import './Novel.sass'
|
import './Novel.sass'
|
||||||
import NotFoundError from "../../components/NotFoundError";
|
import NotFoundError from "../../components/NotFound/NotFoundError";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import {
|
import {
|
||||||
createUserNovel,
|
createUserNovel,
|
||||||
@@ -8,79 +8,103 @@ import {
|
|||||||
fetchNovel,
|
fetchNovel,
|
||||||
fetchNovelCharacters,
|
fetchNovelCharacters,
|
||||||
fetchNovelComments,
|
fetchNovelComments,
|
||||||
fetchNovelGenres,
|
|
||||||
fetchUserNovel,
|
fetchUserNovel,
|
||||||
postNovelComment,
|
postNovelComment,
|
||||||
updateUserNovel
|
updateUserNovel
|
||||||
} from "../../api/novels";
|
} from "../../api/novels";
|
||||||
|
import { TranslateExitStatus, TranslatePlatform, TranslateStatus } from "../../api/api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import { TranslateExitStatus, TranslatePlatform, TranslateStatus } from "../../api/api";
|
|
||||||
import Comments from "../Comments/Comments";
|
|
||||||
import LoadingModal from "../Modals/LoadingModal";
|
|
||||||
import { hideModal, setModal } from "../../store/ducks/modal";
|
import { hideModal, setModal } from "../../store/ducks/modal";
|
||||||
import { hasPermission } from "../../utils";
|
import { hasPermission } from "../../utils";
|
||||||
import ConfirmModal from "../Modals/ConfirmModal";
|
import { setPreloader } from "../../store/ducks/preloader";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Title } from "../../ui/ui";
|
||||||
|
const Comments = React.lazy(() => import('../Comments/Comments'));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentUser: UserType
|
currentUser: Unmei.UserType
|
||||||
setModal: SetModal
|
setModal: SetModal
|
||||||
hideModal: HideModal
|
hideModal: HideModal
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
match: { params: { novelId: number } }
|
match: { params: { novelId: number } }
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
novel: NovelType | null
|
novel: Unmei.NovelType | null
|
||||||
characters: CharacterType[]
|
characters: Unmei.CharacterType[]
|
||||||
genres: GenreType[]
|
|
||||||
|
|
||||||
errorCode: number | null
|
errorCode: number | null
|
||||||
userData: UserNovelType | null
|
userData: Unmei.UserNovelType | null
|
||||||
statusExpanded: boolean
|
statusExpanded: boolean
|
||||||
|
|
||||||
comments: CommentType[]
|
comments: Unmei.CommentType[]
|
||||||
|
commentsLoaded: boolean
|
||||||
commentsOffset: number
|
commentsOffset: number
|
||||||
hasMoreComments: boolean
|
hasMoreComments: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class Novel extends React.Component<Props, State> {
|
class Novel extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
novel: null, characters: [], genres: [],
|
novel: null, characters: [],
|
||||||
errorCode: null, userData: null, statusExpanded: false,
|
errorCode: null, userData: null, statusExpanded: false,
|
||||||
comments: [], commentsOffset: 5, hasMoreComments: true
|
comments: [], commentsLoaded: false, commentsOffset: 5, hasMoreComments: true
|
||||||
};
|
};
|
||||||
|
|
||||||
updateNovelStatus = (status: string) => {
|
countNovelDuration = (t: number) => {
|
||||||
const { match: { params }, currentUser, setModal, hideModal } = this.props;
|
const h = Math.round(t / 60);
|
||||||
|
const m = t - (t * 60);
|
||||||
|
|
||||||
|
if(h === 0) return `${m} мин`;
|
||||||
|
else return `${h} ч`;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNovelMark = (mark: number) => {
|
||||||
|
const { match: { params }, currentUser, setPreloader } = this.props;
|
||||||
const { userData } = this.state;
|
const { userData } = this.state;
|
||||||
|
|
||||||
setModal(<LoadingModal/>);
|
if(!userData) return;
|
||||||
|
if(userData.mark === mark) return;
|
||||||
|
setPreloader(true);
|
||||||
|
updateUserNovel(currentUser.id, params.novelId, { mark }).then(async userData => {
|
||||||
|
const novel = await fetchNovel(params.novelId)
|
||||||
|
novel.release_date = new Date(novel.release_date);
|
||||||
|
this.setState({ novel, userData });
|
||||||
|
setPreloader(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNovelStatus = (status: string) => {
|
||||||
|
const { match: { params }, currentUser, setPreloader } = this.props;
|
||||||
|
const { userData } = this.state;
|
||||||
|
|
||||||
|
setPreloader(true);
|
||||||
if(userData) {
|
if(userData) {
|
||||||
updateUserNovel(currentUser.id, params.novelId, { status }).then(r => {
|
updateUserNovel(currentUser.id, params.novelId, { status }).then(r => {
|
||||||
this.setState({ userData: r, statusExpanded: false });
|
this.setState({ userData: r, statusExpanded: false });
|
||||||
hideModal();
|
setPreloader(false);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
createUserNovel(currentUser.id, params.novelId, status).then(r => {
|
createUserNovel(currentUser.id, params.novelId, status).then(r => {
|
||||||
this.setState({ userData: r, statusExpanded: false });
|
this.setState({ userData: r, statusExpanded: false });
|
||||||
hideModal();
|
setPreloader(false);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteNovelStatus = () => {
|
deleteNovelStatus = () => {
|
||||||
const { match: { params }, currentUser, setModal, hideModal } = this.props;
|
const { match: { params }, currentUser, setPreloader } = this.props;
|
||||||
|
|
||||||
setModal(<LoadingModal/>);
|
setPreloader(true);
|
||||||
deleteUserNovel(currentUser.id, params.novelId).then(() => {
|
deleteUserNovel(currentUser.id, params.novelId).then(() => {
|
||||||
this.setState({ userData: null, statusExpanded: false });
|
this.setState({ userData: null, statusExpanded: false });
|
||||||
hideModal();
|
setPreloader(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
sendComment = (text: string) => {
|
sendComment = (text: string) => {
|
||||||
const { match: { params: { novelId } } } = this.props;
|
const { match: { params: { novelId } } } = this.props;
|
||||||
postNovelComment(novelId, text).then((comment: CommentType) => {
|
postNovelComment(novelId, text).then(comment => {
|
||||||
const { comments } = this.state;
|
const { comments } = this.state;
|
||||||
this.setState({ comments: [comment, ...comments] });
|
this.setState({ comments: [comment, ...comments] });
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
@@ -93,7 +117,7 @@ class Novel extends React.Component<Props, State> {
|
|||||||
fetchNovelComments(params.novelId, commentsOffset, 10).then(r => {
|
fetchNovelComments(params.novelId, commentsOffset, 10).then(r => {
|
||||||
const hasMoreComments = r.count <= commentsOffset;
|
const hasMoreComments = r.count <= commentsOffset;
|
||||||
commentsOffset += 5;
|
commentsOffset += 5;
|
||||||
this.setState({ comments: [...r.comments.reverse(), ...comments], hasMoreComments, commentsOffset })
|
this.setState({ comments: [...r.comments.reverse(), ...comments], hasMoreComments, commentsOffset });
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,70 +125,64 @@ class Novel extends React.Component<Props, State> {
|
|||||||
const { match: { params }, currentUser } = this.props;
|
const { match: { params }, currentUser } = this.props;
|
||||||
const { currentUser: prevUser } = prevProps;
|
const { currentUser: prevUser } = prevProps;
|
||||||
|
|
||||||
if(!prevUser.authorized && currentUser.authorized)
|
if(!prevUser.authorized && currentUser.authorized) {
|
||||||
fetchUserNovel(currentUser.id, params.novelId)
|
fetchUserNovel(currentUser.id, params.novelId).then(userData => {
|
||||||
.then(data => this.setState({ userData: data }))
|
this.setState({ userData });
|
||||||
.catch(err => console.error(err));
|
}).catch((err: Unmei.ApiError) => {
|
||||||
|
if(err.code !== 100)
|
||||||
|
console.error(err.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
else if(prevUser.authorized && !currentUser.authorized)
|
else if(prevUser.authorized && !currentUser.authorized)
|
||||||
this.setState({ userData: null })
|
this.setState({ userData: null })
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
const { match: { params }, currentUser } = this.props;
|
const { match: { params }, currentUser, setPreloader } = this.props;
|
||||||
|
|
||||||
fetchNovel(params.novelId).then((novel: NovelType) => {
|
setPreloader(true);
|
||||||
|
fetchNovel(params.novelId).then(async novel => {
|
||||||
novel.release_date = new Date(novel.release_date);
|
novel.release_date = new Date(novel.release_date);
|
||||||
this.setState({ novel });
|
const r = await fetchNovelComments(params.novelId);
|
||||||
document.title = novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name;
|
const hasMoreComments = r.count > 5;
|
||||||
|
|
||||||
fetchNovelComments(params.novelId).then(r => {
|
const characters = await fetchNovelCharacters(params.novelId);
|
||||||
const hasMoreComments = r.count > 5;
|
this.setState({ novel, characters, comments: r.comments.reverse(), hasMoreComments, commentsLoaded: true });
|
||||||
this.setState({ comments: r.comments.reverse(), hasMoreComments })
|
|
||||||
});
|
setPreloader(false);
|
||||||
fetchNovelCharacters(params.novelId).then(characters => this.setState({ characters }));
|
}).catch((err: Unmei.ApiError) => {
|
||||||
fetchNovelGenres(params.novelId).then(genres => this.setState({ genres }));
|
|
||||||
}).catch((err: ApiError) => {
|
|
||||||
this.setState({ errorCode: err.code });
|
this.setState({ errorCode: err.code });
|
||||||
|
setPreloader(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(currentUser.authorized) {
|
if(currentUser.authorized) {
|
||||||
fetchUserNovel(currentUser.id, params.novelId).then((userData: UserNovelType) => {
|
fetchUserNovel(currentUser.id, params.novelId).then((userData: Unmei.UserNovelType) => {
|
||||||
this.setState({ userData });
|
this.setState({ userData });
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
if(err.code !== 100) console.error(err.text);
|
if(err.code !== 100) console.error(err.text);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNovelMark = (mark: number) => {
|
|
||||||
const { match: { params }, currentUser, hideModal, setModal } = this.props;
|
|
||||||
const { userData } = this.state;
|
|
||||||
|
|
||||||
if(userData?.mark === mark) return;
|
|
||||||
setModal(<LoadingModal/>);
|
|
||||||
updateUserNovel(currentUser.id, params.novelId, { mark }).then(res => {
|
|
||||||
userData!!.mark = mark;
|
|
||||||
this.setState({ userData: res });
|
|
||||||
hideModal();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
countNovelDuration = (t: number) => {
|
|
||||||
const h = Math.round(t / 60);
|
|
||||||
const m = t - (t * 60);
|
|
||||||
|
|
||||||
if(h === 0) return `${m} мин`;
|
|
||||||
else return `${h} ч`;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { errorCode, novel, comments, characters, genres, userData, statusExpanded, hasMoreComments } = this.state;
|
const { errorCode, novel, comments, commentsLoaded, characters, userData, statusExpanded, hasMoreComments } = this.state;
|
||||||
const { currentUser, setModal } = this.props;
|
const { currentUser } = this.props;
|
||||||
|
|
||||||
if(errorCode === 100) return <NotFoundError/>;
|
if(errorCode === 100) return <NotFoundError/>;
|
||||||
if(!novel) return <Loading/>;
|
if(!novel) return <Loading/>;
|
||||||
|
const maxRating = Math.max(...novel.rating_all.map(r => r.count));
|
||||||
|
const maxStatus = Math.max(...novel.status_all.map(r => r.count));
|
||||||
return (
|
return (
|
||||||
<div className={'Novel'}>
|
<div className={'Novel'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name}</title>
|
||||||
|
<meta name='description' content={novel.description}/>
|
||||||
|
|
||||||
|
<meta property="og:title" content={novel.localized_name ? `${novel.original_name} / ${novel.localized_name}` : novel.original_name} />
|
||||||
|
<meta property="og:description" content={novel.description} />
|
||||||
|
<meta property="og:image" content={novel.image} />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className="Novel__Main">
|
<div className="Novel__Main">
|
||||||
<div className={'Novel__Main_Left'}>
|
<div className={'Novel__Main_Left'}>
|
||||||
{novel.image
|
{novel.image
|
||||||
@@ -174,10 +192,8 @@ class Novel extends React.Component<Props, State> {
|
|||||||
<div className={'Novel__Main_Status'}>
|
<div className={'Novel__Main_Status'}>
|
||||||
<div className="Novel__Main_Status_Primary">
|
<div className="Novel__Main_Status_Primary">
|
||||||
{!userData
|
{!userData
|
||||||
? <div className={'Novel__Main_Status_Element'}
|
? <div className={'Novel__Main_Status_Element'} onClick={() => this.updateNovelStatus('planned')}>Добавить в список</div>
|
||||||
onClick={() => this.updateNovelStatus('planned')}>Добавить в список</div>
|
: <div className={"Novel__Main_Status_Element"}>{TranslateStatus[userData.status]}</div>}
|
||||||
: <div
|
|
||||||
className={"Novel__Main_Status_Element"}>{TranslateStatus[userData.status]}</div>}
|
|
||||||
{userData && (
|
{userData && (
|
||||||
<div
|
<div
|
||||||
className={'Novel__Main_Status_Element'}
|
className={'Novel__Main_Status_Element'}
|
||||||
@@ -191,8 +207,7 @@ class Novel extends React.Component<Props, State> {
|
|||||||
{statusExpanded &&
|
{statusExpanded &&
|
||||||
<div className="Novel__Main_Status_Extended">
|
<div className="Novel__Main_Status_Extended">
|
||||||
{userData && Object.entries(TranslateStatus).filter(([eng]) => eng !== userData.status).map(([eng, rus]) => (
|
{userData && Object.entries(TranslateStatus).filter(([eng]) => eng !== userData.status).map(([eng, rus]) => (
|
||||||
<div key={eng} className="Novel__Main_Status_Element"
|
<div key={eng} className="Novel__Main_Status_Element" onClick={() => this.updateNovelStatus(eng)}>{rus}</div>
|
||||||
onClick={() => this.updateNovelStatus(eng)}>{rus}</div>
|
|
||||||
))}
|
))}
|
||||||
<div className={"Novel__Main_Status_Element"} style={{ color: 'red' }}
|
<div className={"Novel__Main_Status_Element"} style={{ color: 'red' }}
|
||||||
onClick={this.deleteNovelStatus}>Удалить из списка
|
onClick={this.deleteNovelStatus}>Удалить из списка
|
||||||
@@ -210,23 +225,31 @@ class Novel extends React.Component<Props, State> {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{novel.walkthrough.length > 0 && <div className="Novel__Main_Status_Mark">
|
||||||
|
<a className="Novel__Main_Status_Mark_Element" target='_blank' rel='noreferrer' href={novel.walkthrough}>Прохождение</a>
|
||||||
|
</div>}
|
||||||
|
|
||||||
|
{novel.links.length > 0 && <div className="Novel__Main_Status_Mark">
|
||||||
|
{novel.links.map(link => (
|
||||||
|
<a className="Novel__Main_Status_Mark_Element" target='_blank' rel='noreferrer' href={link.link} key={link.name}>{link.name}</a>
|
||||||
|
))}
|
||||||
|
</div>}
|
||||||
|
|
||||||
{hasPermission(currentUser, 'novel') && (
|
{hasPermission(currentUser, 'novel') && (
|
||||||
<div className="Novel__Main_Status_Mark">
|
<div className="Novel__Main_Status_Mark">
|
||||||
<Link to={`/kawaii__neko/novels/${novel.id}`} className="Novel__Main_Status_Mark_Element">Изменить</Link>
|
<Link className="Novel__Main_Status_Mark_Element" to={`/kawaii__neko/novels/${novel.id}`}>Изменить</Link>
|
||||||
<div className="Novel__Main_Status_Mark_Element" onClick={() => setModal(<ConfirmModal onConfirm={() => console.log(123)}/>)}>Удалить</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className={'Novel__Info'}>
|
<div className={'Novel__Main_Info'}>
|
||||||
<div className={'Novel__Info_Title'}>{novel.localized_name || novel.original_name}</div>
|
<div className={'Novel__Main_Info_Title'}>{novel.is_demo && <div className='Novel__Main_Info_Demo'>Demo</div>} {novel.localized_name || novel.original_name}</div>
|
||||||
{novel.localized_name && <div className="Novel__Info_OriginalName">
|
{novel.localized_name && <div className="Novel__Main_Info_OriginalName">
|
||||||
<strong>Оригинальное название</strong>: {novel.original_name}
|
<strong>Оригинальное название</strong>: {novel.original_name}
|
||||||
</div>}
|
</div>}
|
||||||
{genres && genres.length > 0 &&
|
{novel.genres.length > 0 &&
|
||||||
<div className="Novel__Info_Genres">
|
<div className="Novel__Main_Info_Genres">
|
||||||
<strong>Жанры</strong>: {genres.map(genre => <div className={'Genre'}
|
<strong>Жанры</strong>: {novel.genres.map(genre => <div className={'Genre'} key={genre.id}>{genre.localized_name}</div>)}
|
||||||
key={genre.id}>{genre.localized_name}</div>)}
|
|
||||||
</div>}
|
</div>}
|
||||||
<div>
|
<div>
|
||||||
<strong>Год выхода</strong>: {novel.release_date.toLocaleDateString()}
|
<strong>Год выхода</strong>: {novel.release_date.toLocaleDateString()}
|
||||||
@@ -240,13 +263,28 @@ class Novel extends React.Component<Props, State> {
|
|||||||
{novel.platforms && <div>
|
{novel.platforms && <div>
|
||||||
<strong>Платформы</strong>: {novel.platforms.split(',').map(platform => TranslatePlatform[platform]).join(', ')}
|
<strong>Платформы</strong>: {novel.platforms.split(',').map(platform => TranslatePlatform[platform]).join(', ')}
|
||||||
</div>}
|
</div>}
|
||||||
{novel.links.length > 0 && <div>
|
<div>
|
||||||
<strong>Ссылки</strong>: {novel.links.map(link => <a key={`${novel.id}_${link.name}`} style={{ margin: '0 .2rem' }} target='_blank' rel="noopener noreferrer" href={link.link}>{link.name}</a>)}
|
|
||||||
</div>}
|
|
||||||
<div className="Novel__Info_Rating">
|
|
||||||
<strong>Ср. оценка</strong>: {novel.rating.toFixed(2)}
|
<strong>Ср. оценка</strong>: {novel.rating.toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<pre className={'Novel__Info_Description'}>{novel.description}</pre>
|
<pre className={'Novel__Main_Info_Description'}>{novel.description}</pre>
|
||||||
|
</div>
|
||||||
|
<div className="Novel__Main_Rating">
|
||||||
|
<div className="Novel__Main_Rating_All">
|
||||||
|
{novel.rating_all.length > 0 && <Title>Оценки людей</Title>}
|
||||||
|
{novel.rating_all.sort((a, b) => b.count-a.count).map(rating => (
|
||||||
|
<div key={rating.mark} className={'Novel__Main_Rating_All_Mark'} style={{ width: `${rating.count / maxRating*100}%` }}>
|
||||||
|
<div>{rating.mark}</div>
|
||||||
|
<div>{rating.count}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{novel.status_all.length > 0 && <Title>В списках</Title>}
|
||||||
|
{novel.status_all.sort((a, b) => b.count-a.count).map(status => (
|
||||||
|
<div key={status.status} className={'Novel__Main_Rating_All_Mark'} style={{ width: `${status.count / maxStatus*100}%`, backgroundColor: `var(--${status.status}-bg-color` }}>
|
||||||
|
<div style={{ overflow: 'hidden' }}>{TranslateStatus[status.status]}</div>
|
||||||
|
<div style={{ marginLeft: 4 }}>{status.count}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{characters && characters.length > 0 &&
|
{characters && characters.length > 0 &&
|
||||||
@@ -254,16 +292,15 @@ class Novel extends React.Component<Props, State> {
|
|||||||
<div className="Novel__Characters_Title">Главные герои:</div>
|
<div className="Novel__Characters_Title">Главные герои:</div>
|
||||||
<div className="Novel__Characters_List">
|
<div className="Novel__Characters_List">
|
||||||
{this.state.characters.filter(char => char.main).map(char =>
|
{this.state.characters.filter(char => char.main).map(char =>
|
||||||
<div className={'Novel__Character'} key={char.id}>
|
<Link to={`/character/${char.id}`} className={'Novel__Character'} key={char.id}>
|
||||||
<div className='Character_Image' style={{ backgroundImage: `url(${char.image})` }}/>
|
<div className='Character_Image' style={{ backgroundImage: `url(${char.image})` }}/>
|
||||||
<Link to={`/character/${char.id}`}>
|
<div className={'Novel__Character_Name'}>{char.localized_name}</div>
|
||||||
<div className={'Novel__Character_Name'}>{char.localized_name}</div>
|
</Link>)}
|
||||||
</Link>
|
|
||||||
</div>)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
<Comments user={currentUser} comments={comments} sendComment={this.sendComment}
|
<Suspense fallback={<Loading/>}>
|
||||||
loadMore={this.loadMoreComments} hasMore={hasMoreComments}/>
|
<Comments user={currentUser} comments={comments} commentsLoaded={commentsLoaded} sendComment={this.sendComment} loadMore={this.loadMoreComments} hasMore={hasMoreComments}/>
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -275,6 +312,7 @@ export default connect(
|
|||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
setModal: (modal: React.ReactNode | null) => dispatch(setModal(modal)),
|
||||||
hideModal: () => dispatch(hideModal())
|
hideModal: () => dispatch(hideModal()),
|
||||||
|
setPreloader: (status: boolean) => dispatch(setPreloader(status))
|
||||||
})
|
})
|
||||||
)(Novel);
|
)(Novel);
|
||||||
|
|||||||
@@ -6,14 +6,26 @@
|
|||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
display: flex
|
display: flex
|
||||||
|
|
||||||
|
&__Search
|
||||||
|
font-size: 1.5rem
|
||||||
|
font-weight: 500
|
||||||
|
|
||||||
@media (max-width: 600px)
|
@media (max-width: 600px)
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
.Novels__List
|
.Novels__List
|
||||||
display: flex
|
margin-right: 8px
|
||||||
flex-wrap: wrap
|
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
&_Items
|
||||||
|
display: flex
|
||||||
|
flex-wrap: wrap
|
||||||
|
|
||||||
|
.Novels__Search
|
||||||
|
width: 100%
|
||||||
|
height: 32px
|
||||||
|
margin: 8px
|
||||||
|
|
||||||
@media (max-width: 600px)
|
@media (max-width: 600px)
|
||||||
justify-content: center
|
justify-content: center
|
||||||
|
|
||||||
@@ -36,14 +48,17 @@
|
|||||||
|
|
||||||
.Novels__Filters
|
.Novels__Filters
|
||||||
padding: $primaryPadding
|
padding: $primaryPadding
|
||||||
|
box-sizing: border-box
|
||||||
background-color: var(--block-color)
|
background-color: var(--block-color)
|
||||||
border-radius: $borderRadius
|
border-radius: $borderRadius
|
||||||
width: calc(100% / 4 - 1rem)
|
width: calc(100% / 4 - 1rem)
|
||||||
margin-left: auto
|
height: fit-content
|
||||||
|
|
||||||
@media (max-width: 600px)
|
@media (max-width: 600px)
|
||||||
align-self: center
|
align-self: center
|
||||||
order: -1
|
order: -1
|
||||||
|
width: 100%
|
||||||
|
margin: 8px
|
||||||
|
|
||||||
div
|
div
|
||||||
margin: 0 0 .5rem 0
|
margin: 0 0 .5rem 0
|
||||||
|
|||||||
+117
-33
@@ -1,75 +1,159 @@
|
|||||||
import './Novels.sass'
|
import './Novels.sass'
|
||||||
import React from "react";
|
import React, { Suspense } from "react";
|
||||||
import { fetchNovels } from "../../api/novels";
|
import { fetchNovels, searchNovel } from "../../api/novels";
|
||||||
import NovelItem from '../../ui/NovelItem/NovelItem';
|
import { fetchGenres } from "../../api/genres";
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setPreloader } from "../../store/ducks/preloader";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { Input } from "../../ui/ui";
|
||||||
|
const NovelItem = React.lazy(() => import('../../ui/NovelItem/NovelItem'))
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
novels: NovelType[] | null
|
novels: Unmei.NovelType[] | null
|
||||||
years: string[]
|
lastSearch: number
|
||||||
|
allGenres: Unmei.GenreType[]
|
||||||
|
years: number[]
|
||||||
genres: string[]
|
genres: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Novels extends React.Component<{}, State> {
|
class Novels extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
novels: null,
|
novels: null, lastSearch: 0,
|
||||||
|
allGenres: [],
|
||||||
genres: [], years: []
|
genres: [], years: []
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount = () => {
|
componentDidMount = () => {
|
||||||
fetchNovels('rating').then((novels: NovelType[]) => {
|
const { setPreloader } = this.props;
|
||||||
|
|
||||||
|
setPreloader(true);
|
||||||
|
fetchNovels('rating').then(async novels => {
|
||||||
novels.forEach(novel => {
|
novels.forEach(novel => {
|
||||||
novel.release_date = new Date(novel.release_date);
|
novel.release_date = new Date(novel.release_date);
|
||||||
});
|
});
|
||||||
this.setState({ novels });
|
|
||||||
document.title = 'Новелы';
|
const allGenres = await fetchGenres();
|
||||||
})
|
|
||||||
|
this.setState({ novels, allGenres });
|
||||||
|
setPreloader(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
switchFilter(filter: string, key: any) {
|
switchFilter(filter: string, key: any) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const filters: string[] = this.state[filter];
|
let filters: string[] = this.state[filter];
|
||||||
|
|
||||||
|
filters = filters as string[];
|
||||||
if(filters.includes(key)) {
|
if(filters.includes(key)) {
|
||||||
// @ts-ignore
|
this.setState({ [filter]: [...filters.filter(x => x !== key)] } as Pick<State, any>)
|
||||||
this.setState({ [filter]: [...filters.filter(x => x !== key)] })
|
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
this.setState({ [filter]: [...filters, key] } as Pick<State, any>)
|
||||||
this.setState({ [filter]: [...filters, key] })
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
searchNovels = (q: string) => {
|
||||||
|
if(q.length === 0) {
|
||||||
|
fetchNovels('rating').then(async novels => {
|
||||||
|
novels.forEach(novel => {
|
||||||
|
novel.release_date = new Date(novel.release_date);
|
||||||
|
});
|
||||||
|
|
||||||
|
const allGenres = await fetchGenres();
|
||||||
|
this.setState({ novels, allGenres });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
searchNovel(q).then(novels => {
|
||||||
|
novels.forEach(novel => {
|
||||||
|
novel.release_date = new Date(novel.release_date);
|
||||||
|
});
|
||||||
|
this.setState({ novels });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { novels } = this.state;
|
let { novels } = this.state;
|
||||||
if(!novels)
|
const { years, genres, allGenres } = this.state;
|
||||||
return <Loading/>;
|
if(!novels) return <Loading/>;
|
||||||
/*const filteredNovels = novels.filter(novel => {
|
|
||||||
if(years.includes(novel.release_date.getFullYear().toString())) {
|
novels = novels
|
||||||
return true;
|
.filter(novel => {
|
||||||
}
|
if(years.length === 0) return novel;
|
||||||
return false;
|
return years.includes(novel.release_date.getFullYear())
|
||||||
});*/
|
})
|
||||||
|
.filter(novel => {
|
||||||
|
if(genres.length === 0) return novel;
|
||||||
|
if(novel.genres.filter(genre => genres.includes(genre.name)).length > 0) return novel;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'Novels'}>
|
<div className={'Novels'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Новеллы</title>
|
||||||
|
<meta name='description' content=''/>
|
||||||
|
</Helmet>
|
||||||
<div className="Novels__List">
|
<div className="Novels__List">
|
||||||
{novels.map((novel: NovelType) => <NovelItem {...novel} key={novel.id} viewType={'grid'}/>)}
|
<Input placeholder={'Поиск...'} className={'Novels__Search'} onChange={e => this.searchNovels(e.target.value)}/>
|
||||||
|
<div className="Novels__List_Items">
|
||||||
|
<Suspense fallback={<Loading/>}>
|
||||||
|
{novels.length === 0 && (
|
||||||
|
<div style={{ textAlign: 'center', width: '100%' }}>По данным фильтрам ничего не найдено...</div>
|
||||||
|
)}
|
||||||
|
{novels.map(novel => <NovelItem {...novel} key={novel.id} viewType={'grid'}/>)}
|
||||||
|
</Suspense>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/*<div className="Novels__Filters">
|
<div className="Novels__Filters">
|
||||||
<div className="Novels__Filters_Genre">
|
<div className="Novels__Filters_Genre">
|
||||||
<div className="Novels__Filters_Title">Жанры</div>
|
<div className="Novels__Filters_Title">Жанры</div>
|
||||||
<label><input type="checkbox"/>Романтика</label>
|
{allGenres.map(genre => (
|
||||||
<label><input type="checkbox"/>Детектив</label>
|
<label key={genre.id}>
|
||||||
|
<input type="checkbox" onChange={() => this.switchFilter('genres', genre.name)}/>
|
||||||
|
{genre.localized_name}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="Novels__Filters_Year">
|
<div className="Novels__Filters_Year">
|
||||||
<div className="Novels__Filters_Title">Год выпуска</div>
|
<div className="Novels__Filters_Title">Год выпуска</div>
|
||||||
<label><input type="checkbox" onClick={() => this.switchFilter('years', '2020')}/>2020</label>
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2021)}/>2021</label>
|
||||||
<label><input type="checkbox" onClick={() => this.switchFilter('years', '2019')}/>2019</label>
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2020)}/>2020</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2019)}/>2019</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2018)}/>2018</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2017)}/>2017</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2016)}/>2016</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2015)}/>2015</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2014)}/>2014</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2013)}/>2013</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2012)}/>2012</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2011)}/>2011</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2010)}/>2010</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2009)}/>2009</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2008)}/>2008</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2007)}/>2007</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2006)}/>2006</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2005)}/>2005</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2004)}/>2004</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2003)}/>2003</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2002)}/>2002</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2001)}/>2001</label>
|
||||||
|
<label><input type="checkbox" onClick={() => this.switchFilter('years', 2000)}/>2000</label>
|
||||||
</div>
|
</div>
|
||||||
</div>*/}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Novels;
|
export default connect(
|
||||||
|
null,
|
||||||
|
dispatch => ({
|
||||||
|
setPreloader: (s: boolean) => dispatch(setPreloader(s))
|
||||||
|
})
|
||||||
|
)(Novels);
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+1
-1
@@ -36,7 +36,7 @@ class PasswordRestore extends React.Component<Props, State> {
|
|||||||
|
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
const error = errors[err.code];
|
const error = errors[err.code];
|
||||||
if(error) this.setState({ error });
|
if(error) this.setState({ error });
|
||||||
else this.setState({ error: `Произошла ошибка! Сообщите номер и текст разработчику!\nКод: ${err.code}\nТекст: ${err.text}` })
|
else this.setState({ error: `Произошла ошибка! Сообщите номер и текст разработчику!\nКод: ${err.code}\nТекст: ${err.text}` })
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
-1
@@ -36,7 +36,6 @@
|
|||||||
|
|
||||||
&_Novels
|
&_Novels
|
||||||
display: flex
|
display: flex
|
||||||
justify-content: center
|
|
||||||
|
|
||||||
&.Grid
|
&.Grid
|
||||||
flex-direction: row
|
flex-direction: row
|
||||||
|
|||||||
Regular → Executable
+20
-11
@@ -1,20 +1,21 @@
|
|||||||
import React from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import './UserNovels.sass';
|
import './UserNovels.sass';
|
||||||
import { fetchUser, fetchUserNovels } from '../../../api/users';
|
import { fetchUser, fetchUserNovels } from '../../../api/users';
|
||||||
import Loading from '../../../components/Loading';
|
import Loading from '../../../components/Loading';
|
||||||
import NovelItem from '../../../ui/NovelItem/NovelItem';
|
|
||||||
import { capitalize, generateClassName, getRandomInt } from "../../../utils";
|
import { capitalize, generateClassName, getRandomInt } from "../../../utils";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faAlignJustify, faThLarge } from "@fortawesome/free-solid-svg-icons";
|
import { faAlignJustify, faThLarge } from "@fortawesome/free-solid-svg-icons";
|
||||||
import Button from "../../../ui/Button/Button";
|
import Button from "../../../ui/Button/Button";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
const NovelItem = React.lazy(() => import('../../../ui/NovelItem/NovelItem'));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: number } }
|
match: { params: { userId: number } }
|
||||||
history: { push: (path: string) => void }
|
history: { push: (path: string) => void }
|
||||||
};
|
};
|
||||||
type State = {
|
type State = {
|
||||||
novels: NovelType[]
|
novels: Unmei.NovelType[]
|
||||||
user: UserType | null
|
user: Unmei.UserType | null
|
||||||
viewType: string
|
viewType: string
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,11 +35,9 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { match: { params: { userId } } } = this.props;
|
const { match: { params: { userId } } } = this.props;
|
||||||
|
|
||||||
fetchUserNovels(userId)
|
fetchUserNovels(userId).then(novels => this.setState({ novels }));
|
||||||
.then(novels => this.setState({ novels }));
|
|
||||||
|
|
||||||
fetchUser(userId)
|
fetchUser(userId).then(user => this.setState({ user }));
|
||||||
.then(user => this.setState({ user }));
|
|
||||||
|
|
||||||
const viewType = localStorage.getItem('viewType');
|
const viewType = localStorage.getItem('viewType');
|
||||||
if(viewType) {
|
if(viewType) {
|
||||||
@@ -66,6 +65,10 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='UserNovels'>
|
<div className='UserNovels'>
|
||||||
|
<Helmet>
|
||||||
|
<title>{user.username} / Новеллы</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div className="UserNovels__Title">
|
<div className="UserNovels__Title">
|
||||||
Новеллы пользователя {user.username}
|
Новеллы пользователя {user.username}
|
||||||
<FontAwesomeIcon icon={faThLarge} onClick={() => this.changeViewType('grid')}/>
|
<FontAwesomeIcon icon={faThLarge} onClick={() => this.changeViewType('grid')}/>
|
||||||
@@ -78,7 +81,9 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
<Button onClick={this.getRandomNovel}>Рандомная новелла</Button>
|
<Button onClick={this.getRandomNovel}>Рандомная новелла</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
<Suspense fallback={<Loading/>}>
|
||||||
|
{planned.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
@@ -87,7 +92,9 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
Пройденные
|
Пройденные
|
||||||
</div>
|
</div>
|
||||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{completed.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
<Suspense fallback={<Loading/>}>
|
||||||
|
{completed.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
@@ -96,7 +103,9 @@ class UserNovels extends React.Component<Props, State> {
|
|||||||
Прохожу
|
Прохожу
|
||||||
</div>
|
</div>
|
||||||
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
<div className={generateClassName("UserNovels__List_Novels", capitalize(viewType) || 'Grid')}>
|
||||||
{in_progress.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
<Suspense fallback={<Loading/>}>
|
||||||
|
{in_progress.map((novel, id) => <NovelItem {...novel} key={novel.id} novelId={id + 1} viewType={viewType}/>)}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Regular → Executable
+4
-4
@@ -10,7 +10,7 @@ import TextField from "../../../ui/TextField/TextField";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setUser: SetUser
|
setUser: SetUser
|
||||||
settings: UserSettingsType
|
settings: Unmei.UserSettingsType
|
||||||
|
|
||||||
snowfallSettings: Snowfall
|
snowfallSettings: Snowfall
|
||||||
setSnowflakeCount: SetSnowflakeCount
|
setSnowflakeCount: SetSnowflakeCount
|
||||||
@@ -18,7 +18,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
theme: Theme
|
theme: Unmei.Theme
|
||||||
accentColor: string
|
accentColor: string
|
||||||
customTheme: string
|
customTheme: string
|
||||||
colorName: boolean
|
colorName: boolean
|
||||||
@@ -75,7 +75,7 @@ class Appearance extends React.Component<Props, State> {
|
|||||||
return (<>
|
return (<>
|
||||||
<Group title={'Тема'} className={'Settings_Group'} columns={3}>
|
<Group title={'Тема'} className={'Settings_Group'} columns={3}>
|
||||||
<form onSubmit={this.saveTheme}>
|
<form onSubmit={this.saveTheme}>
|
||||||
<select onChange={e => this.setState({ theme: e.target.value as Theme })} value={theme}>
|
<select onChange={e => this.setState({ theme: e.target.value as Unmei.Theme })} value={theme}>
|
||||||
<option value="dark">Dark</option>
|
<option value="dark">Dark</option>
|
||||||
<option value="blue">Blue</option>
|
<option value="blue">Blue</option>
|
||||||
<option value="red">Red</option>
|
<option value="red">Red</option>
|
||||||
@@ -164,7 +164,7 @@ export default connect(
|
|||||||
snowfallSettings: state.snowfall
|
snowfallSettings: state.snowfall
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (user: UserType) => dispatch(setUser(user)),
|
setUser: (user: Unmei.UserType) => dispatch(setUser(user)),
|
||||||
setSnowflakeCount: (count: number) => dispatch(setSnowflakeCount(count)),
|
setSnowflakeCount: (count: number) => dispatch(setSnowflakeCount(count)),
|
||||||
setSnowfallStatus: (status: boolean) => dispatch(setSnowfallStatus(status))
|
setSnowfallStatus: (status: boolean) => dispatch(setSnowfallStatus(status))
|
||||||
})
|
})
|
||||||
|
|||||||
Regular → Executable
+29
-11
@@ -3,21 +3,26 @@ import Button from "../../../ui/Button/Button";
|
|||||||
import Group from "../../../ui/Group/Group";
|
import Group from "../../../ui/Group/Group";
|
||||||
import { updateUserGeneralSettings, uploadAvatar } from "../../../api/users";
|
import { updateUserGeneralSettings, uploadAvatar } from "../../../api/users";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
|
import { hasPermission } from "../../../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
user: Unmei.UserType
|
||||||
setUser: SetUser
|
setUser: SetUser
|
||||||
settings: UserSettingsType
|
settings: Unmei.UserSettingsType
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
// Avatar
|
// Avatar
|
||||||
useGravatar: boolean
|
useGravatar: boolean
|
||||||
avatar: File | null
|
avatar: File | null
|
||||||
|
|
||||||
|
error: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class General extends React.Component<Props, State> {
|
class General extends React.Component<Props, State> {
|
||||||
state: State = {
|
state: State = {
|
||||||
useGravatar: this.props.settings.use_gravatar, avatar: null
|
useGravatar: this.props.settings.use_gravatar, avatar: null,
|
||||||
|
error: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeAvatar = (event: ChangeEvent<HTMLInputElement>) => {
|
onChangeAvatar = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -29,14 +34,20 @@ class General extends React.Component<Props, State> {
|
|||||||
saveAvatar = (event: FormEvent) => {
|
saveAvatar = (event: FormEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { avatar, useGravatar } = this.state;
|
const { avatar, useGravatar } = this.state;
|
||||||
const { setUser } = this.props;
|
const { setUser, user } = this.props;
|
||||||
|
|
||||||
|
let allowedTypes = ['image/png', 'image/jpeg'];
|
||||||
if(avatar) {
|
if(avatar) {
|
||||||
|
if(hasPermission(user, 'gif')) allowedTypes.push('image/gif');
|
||||||
|
if(!allowedTypes.includes(avatar.type)) {
|
||||||
|
this.setState({ error: 'GIF аватары доступны только людям, с группой Supporter!' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('avatar', avatar);
|
formData.append('avatar', avatar);
|
||||||
|
|
||||||
uploadAvatar(formData).then(avatarUrl => {
|
uploadAvatar(formData).then(avatarUrl => {
|
||||||
updateUserGeneralSettings(useGravatar, avatarUrl).then(setUser)
|
updateUserGeneralSettings(useGravatar, avatarUrl).then(setUser);
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
updateUserGeneralSettings(useGravatar, '').then(setUser)
|
updateUserGeneralSettings(useGravatar, '').then(setUser)
|
||||||
@@ -44,16 +55,22 @@ class General extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { useGravatar } = this.state;
|
const { useGravatar, error } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={'Аватар'} className={'Settings_Group'}>
|
<Group title={'Аватар'} className={'Settings_Group'}>
|
||||||
|
{error.length > 0 && (
|
||||||
|
<div>{error}</div>
|
||||||
|
)}
|
||||||
<form onSubmit={this.saveAvatar}>
|
<form onSubmit={this.saveAvatar}>
|
||||||
<label>Использовать Gravatar <input type="checkbox"
|
<label>
|
||||||
onChange={event => this.setState({ useGravatar: event.target.checked })}
|
Использовать Gravatar
|
||||||
checked={useGravatar}/></label>
|
<input type="checkbox" onChange={event => this.setState({ useGravatar: event.target.checked })} checked={useGravatar}/>
|
||||||
<label>Загрузить аватар <input type="file" onChange={this.onChangeAvatar}
|
</label>
|
||||||
disabled={useGravatar}/></label>
|
<label>
|
||||||
|
Загрузить аватар
|
||||||
|
<input type="file" onChange={this.onChangeAvatar} disabled={useGravatar}/>
|
||||||
|
</label>
|
||||||
<Button>Сохранить</Button>
|
<Button>Сохранить</Button>
|
||||||
</form>
|
</form>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -63,6 +80,7 @@ class General extends React.Component<Props, State> {
|
|||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
(state: StoreState) => ({
|
(state: StoreState) => ({
|
||||||
settings: state.userSettings
|
settings: state.userSettings,
|
||||||
|
user: state.currentUser
|
||||||
})
|
})
|
||||||
)(General);
|
)(General);
|
||||||
|
|||||||
Regular → Executable
+2
-2
@@ -55,7 +55,7 @@ class Security extends React.Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
oldEmail: '', email1: '', email2: ''
|
oldEmail: '', email1: '', email2: ''
|
||||||
});
|
});
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
this.setState({ emailError: errors[err.code] })
|
this.setState({ emailError: errors[err.code] })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ class Security extends React.Component<Props, State> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
oldPassword: '', password1: '', password2: ''
|
oldPassword: '', password1: '', password2: ''
|
||||||
});
|
});
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
this.setState({ passwordError: errors[err.code] })
|
this.setState({ passwordError: errors[err.code] })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+10
-5
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Group from "../../../ui/Group/Group";
|
import Group from "../../../ui/Group/Group";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import NotFoundError from "../../../components/NotFoundError";
|
import NotFoundError from "../../../components/NotFound/NotFoundError";
|
||||||
import './Settings.sass';
|
import './Settings.sass';
|
||||||
import Tabs from "../../../ui/Tabs/Tabs";
|
import Tabs from "../../../ui/Tabs/Tabs";
|
||||||
import TabItem from "../../../ui/Tabs/TabItem";
|
import TabItem from "../../../ui/Tabs/TabItem";
|
||||||
@@ -9,6 +9,7 @@ import Appearance from "./Appearance";
|
|||||||
import General from "./General";
|
import General from "./General";
|
||||||
import Security from "./Security";
|
import Security from "./Security";
|
||||||
import { setUser } from "../../../store/ducks/currentUser";
|
import { setUser } from "../../../store/ducks/currentUser";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
enum TabsNames {
|
enum TabsNames {
|
||||||
General,
|
General,
|
||||||
@@ -18,8 +19,8 @@ enum TabsNames {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: string } }
|
match: { params: { userId: string } }
|
||||||
currentUser: UserType
|
currentUser: Unmei.UserType
|
||||||
setUser: (user: UserType) => void
|
setUser: (user: Unmei.UserType) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -38,7 +39,11 @@ class Settings extends React.Component<Props, State> {
|
|||||||
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
|
if(parseInt(userId) !== currentUser.id) return <NotFoundError/>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group title={''}>
|
<Group>
|
||||||
|
<Helmet>
|
||||||
|
<title>Настройки</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem
|
<TabItem
|
||||||
selected={currentTab === TabsNames.General}
|
selected={currentTab === TabsNames.General}
|
||||||
@@ -78,6 +83,6 @@ export default connect(
|
|||||||
currentUser: state.currentUser
|
currentUser: state.currentUser
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
setUser: (user: UserType) => dispatch(setUser(user))
|
setUser: (user: Unmei.UserType) => dispatch(setUser(user))
|
||||||
})
|
})
|
||||||
)(Settings);
|
)(Settings);
|
||||||
|
|||||||
Regular → Executable
+8
-8
@@ -1,4 +1,4 @@
|
|||||||
@import "../App/App"
|
@import "../../components/App/App"
|
||||||
|
|
||||||
.User
|
.User
|
||||||
&__Activate
|
&__Activate
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
flex-direction: column
|
flex-direction: column
|
||||||
|
|
||||||
&_Cover
|
&_Cover
|
||||||
height: 20rem
|
height: 320px
|
||||||
width: 100%
|
width: 100%
|
||||||
background-position: center
|
background-position: center
|
||||||
background-size: cover
|
background-size: cover
|
||||||
background-repeat: no-repeat
|
background-repeat: no-repeat
|
||||||
|
|
||||||
@media (max-width: 500px)
|
@media (max-width: 500px)
|
||||||
height: 10rem !important
|
height: 160px !important
|
||||||
|
|
||||||
&_Main
|
&_Main
|
||||||
display: flex
|
display: flex
|
||||||
@@ -121,19 +121,19 @@
|
|||||||
border-radius: 0 0 $borderRadius $borderRadius
|
border-radius: 0 0 $borderRadius $borderRadius
|
||||||
|
|
||||||
.Planned
|
.Planned
|
||||||
background: #339999
|
background: var(--planned-bg-color)
|
||||||
|
|
||||||
.Completed
|
.Completed
|
||||||
background: #339933
|
background: var(--completed-bg-color)
|
||||||
|
|
||||||
.Dropped
|
.Dropped
|
||||||
background: #993333
|
background: var(--dropped-bg-color)
|
||||||
|
|
||||||
.InProgress
|
.InProgress
|
||||||
background: #993399
|
background: var(--in_progress-bg-color)
|
||||||
|
|
||||||
.Deferred
|
.Deferred
|
||||||
background: #333333
|
background: var(--deferred-bg-color)
|
||||||
|
|
||||||
.Planned:after, .Completed:after, .Dropped:after, .InProgress:after, .Deferred:after
|
.Planned:after, .Completed:after, .Dropped:after, .InProgress:after, .Deferred:after
|
||||||
content: attr(data-count)
|
content: attr(data-count)
|
||||||
|
|||||||
Regular → Executable
+25
-31
@@ -1,7 +1,7 @@
|
|||||||
import React, { CSSProperties } from "react";
|
import React, { CSSProperties } from "react";
|
||||||
import { fetchUser, fetchUserNovels, generateActivateLink } from "../../api/users";
|
import { fetchUser, fetchUserNovels, generateActivateLink } from "../../api/users";
|
||||||
import './User.sass'
|
import './User.sass'
|
||||||
import NotFoundError from "../../components/NotFoundError";
|
import NotFoundError from "../../components/NotFound/NotFoundError";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import NovelItem from "../../ui/NovelItem/NovelItem";
|
import NovelItem from "../../ui/NovelItem/NovelItem";
|
||||||
@@ -9,16 +9,19 @@ import { connect } from "react-redux";
|
|||||||
import Button from "../../ui/Button/Button";
|
import Button from "../../ui/Button/Button";
|
||||||
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
import NotificationMessage from "../../ui/Notifications/NotificationMessage";
|
||||||
import { addNotification } from "../../store/ducks/notifications";
|
import { addNotification } from "../../store/ducks/notifications";
|
||||||
|
import { setPreloader } from "../../store/ducks/preloader";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
match: { params: { userId: string } }
|
match: { params: { userId: string } }
|
||||||
currentUser: UserType
|
currentUser: Unmei.UserType
|
||||||
addNotification: AddNotification
|
addNotification: AddNotification
|
||||||
|
setPreloader: SetPreloaderStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
user: UserType | null
|
user: Unmei.UserType | null
|
||||||
novels: NovelType[]
|
novels: Unmei.NovelType[]
|
||||||
errorCode: number | null
|
errorCode: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,14 +31,20 @@ class User extends React.Component<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getAndSetUser = (id: number) => {
|
getAndSetUser = (id: number) => {
|
||||||
|
const { setPreloader } = this.props;
|
||||||
|
|
||||||
|
setPreloader(true);
|
||||||
|
|
||||||
fetchUser(id).then(user => {
|
fetchUser(id).then(user => {
|
||||||
this.setState({ user });
|
this.setState({ user });
|
||||||
document.title = `${user.username} / Unmei`;
|
document.title = `${user.username} / Unmei`;
|
||||||
|
|
||||||
fetchUserNovels(id).then(novels => {
|
fetchUserNovels(id).then(novels => {
|
||||||
this.setState({ novels });
|
this.setState({ novels });
|
||||||
}).catch((err: ApiError) => console.error(err));
|
}).then(() => {
|
||||||
}).catch((err: ApiError) => this.setState({ errorCode: err.code }));
|
setPreloader(false);
|
||||||
|
}).catch((err: Unmei.ApiError) => console.error(err));
|
||||||
|
}).catch((err: Unmei.ApiError) => this.setState({ errorCode: err.code }));
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, _?: any) {
|
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>, _?: any) {
|
||||||
@@ -48,9 +57,9 @@ class User extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const params = this.props.match.params;
|
const { match: { params } } = this.props;
|
||||||
|
|
||||||
this.getAndSetUser(parseInt(params.userId))
|
this.getAndSetUser(parseInt(params.userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
sendActivateLink = () => {
|
sendActivateLink = () => {
|
||||||
@@ -61,7 +70,7 @@ class User extends React.Component<Props, State> {
|
|||||||
</NotificationMessage>
|
</NotificationMessage>
|
||||||
);
|
);
|
||||||
this.props.addNotification(notification);
|
this.props.addNotification(notification);
|
||||||
}).catch((err: ApiError) => {
|
}).catch((err: Unmei.ApiError) => {
|
||||||
const notification = (
|
const notification = (
|
||||||
<NotificationMessage level={"error"}>
|
<NotificationMessage level={"error"}>
|
||||||
Произошла ошибка! №{err.code} <br/>
|
Произошла ошибка! №{err.code} <br/>
|
||||||
@@ -83,6 +92,11 @@ class User extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'User'}>
|
<div className={'User'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{user.username}</title>
|
||||||
|
<meta name='description' content={`Профиль пользователя ${user.username}`}/>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
{!user.is_activated && user.id === currentUser.id && (
|
{!user.is_activated && user.id === currentUser.id && (
|
||||||
<div className={'User__Activate'}>
|
<div className={'User__Activate'}>
|
||||||
<h2>Ваш аккаунт не активирован!</h2>
|
<h2>Ваш аккаунт не активирован!</h2>
|
||||||
@@ -167,27 +181,6 @@ class User extends React.Component<Props, State> {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*<div className="User__List">
|
|
||||||
<div className="User__List_Title">Список аниме пользователя:</div>
|
|
||||||
<div className="User__List_Items">В разработке...</div>
|
|
||||||
</div>
|
|
||||||
<div className="User__List">
|
|
||||||
<div className="User__List_Title">Список манги и ранобэ пользователя:</div>
|
|
||||||
<div className="User__List_Items">В разработке...</div>
|
|
||||||
</div>
|
|
||||||
<div className="User__List">
|
|
||||||
<div className="User__List_Title">Список игр пользователя:</div>
|
|
||||||
<div className="User__List_Items">В разработке...</div>
|
|
||||||
</div>
|
|
||||||
<div className="User__List">
|
|
||||||
<div className="User__List_Title">Список фильмов пользователя:</div>
|
|
||||||
<div className="User__List_Items">В разработке...</div>
|
|
||||||
</div>
|
|
||||||
<div className="User__List">
|
|
||||||
<div className="User__List_Title">Список сериалов пользователя:</div>
|
|
||||||
<div className="User__List_Items">В разработке...</div>
|
|
||||||
</div>*/}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -198,6 +191,7 @@ export default connect(
|
|||||||
currentUser: state.currentUser
|
currentUser: state.currentUser
|
||||||
}),
|
}),
|
||||||
dispatch => ({
|
dispatch => ({
|
||||||
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification))
|
addNotification: (notification: React.ReactNode) => dispatch(addNotification(notification)),
|
||||||
|
setPreloader: (status: boolean) => dispatch(setPreloader(status))
|
||||||
})
|
})
|
||||||
)(User);
|
)(User);
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+6
-1
@@ -3,11 +3,12 @@ import { fetchUsers } from "../../api/users";
|
|||||||
import Loading from "../../components/Loading";
|
import Loading from "../../components/Loading";
|
||||||
import './Users.sass';
|
import './Users.sass';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
|
||||||
type Props = {}
|
type Props = {}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
users: UserType[]
|
users: Unmei.UserType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
class Users extends React.Component<Props, State> {
|
class Users extends React.Component<Props, State> {
|
||||||
@@ -25,6 +26,10 @@ class Users extends React.Component<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'Users'}>
|
<div className={'Users'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>Пользователи</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
{users.map(user => (
|
{users.map(user => (
|
||||||
<Link className={'Users__User'} key={user.id} to={`user/${user.id}`}>
|
<Link className={'Users__User'} key={user.id} to={`user/${user.id}`}>
|
||||||
<div className="Users__User_Avatar"
|
<div className="Users__User_Avatar"
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Title from "../../ui/Title/Title";
|
||||||
|
import { registerUserVk } from "../../api/auth";
|
||||||
|
|
||||||
|
class VKAuth extends React.Component {
|
||||||
|
componentDidMount() {
|
||||||
|
console.log()
|
||||||
|
registerUserVk(window.location.href.split('?')[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Title>{window.location.href}</Title>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VKAuth;
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/// <reference lib="webworker" />
|
||||||
|
/* eslint-disable no-restricted-globals */
|
||||||
|
|
||||||
|
// This service worker can be customized!
|
||||||
|
// See https://developers.google.com/web/tools/workbox/modules
|
||||||
|
// for the list of available Workbox modules, or add any other
|
||||||
|
// code you'd like.
|
||||||
|
// You can also remove this file if you'd prefer not to use a
|
||||||
|
// service worker, and the Workbox build step will be skipped.
|
||||||
|
|
||||||
|
import { clientsClaim } from 'workbox-core';
|
||||||
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
|
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
|
||||||
|
import { registerRoute } from 'workbox-routing';
|
||||||
|
import { StaleWhileRevalidate } from 'workbox-strategies';
|
||||||
|
|
||||||
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
clientsClaim();
|
||||||
|
|
||||||
|
// Precache all of the assets generated by your build process.
|
||||||
|
// Their URLs are injected into the manifest variable below.
|
||||||
|
// This variable must be present somewhere in your service worker file,
|
||||||
|
// even if you decide not to use precaching. See https://cra.link/PWA
|
||||||
|
precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
|
// Set up App Shell-style routing, so that all navigation requests
|
||||||
|
// are fulfilled with your index.html shell. Learn more at
|
||||||
|
// https://developers.google.com/web/fundamentals/architecture/app-shell
|
||||||
|
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
|
||||||
|
registerRoute(
|
||||||
|
// Return false to exempt requests from being fulfilled by index.html.
|
||||||
|
({ request, url }: { request: Request; url: URL }) => {
|
||||||
|
// If this isn't a navigation, skip.
|
||||||
|
if (request.mode !== 'navigate') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a URL that starts with /_, skip.
|
||||||
|
if (url.pathname.startsWith('/_')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this looks like a URL for a resource, because it contains
|
||||||
|
// a file extension, skip.
|
||||||
|
if (url.pathname.match(fileExtensionRegexp)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true to signal that we want to use the handler.
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||||
|
);
|
||||||
|
|
||||||
|
// An example runtime caching route for requests that aren't handled by the
|
||||||
|
// precache, in this case same-origin .png requests like those from in public/
|
||||||
|
registerRoute(
|
||||||
|
// Add in any other file extensions or routing criteria as needed.
|
||||||
|
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
||||||
|
// Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||||
|
new StaleWhileRevalidate({
|
||||||
|
cacheName: 'images',
|
||||||
|
plugins: [
|
||||||
|
// Ensure that once this runtime cache reaches a maximum size the
|
||||||
|
// least-recently used images are removed.
|
||||||
|
new ExpirationPlugin({ maxEntries: 50 }),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// This allows the web app to trigger skipWaiting via
|
||||||
|
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Any other custom service worker logic can go here.
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
const isLocalhost = Boolean(
|
|
||||||
window.location.hostname === 'localhost' ||
|
|
||||||
window.location.hostname === '[::1]' ||
|
|
||||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
|
||||||
);
|
|
||||||
|
|
||||||
type Config = {
|
|
||||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
|
||||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function register(config?: Config) {
|
|
||||||
if(process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
|
||||||
const publicUrl = new URL(
|
|
||||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
|
||||||
window.location.href
|
|
||||||
);
|
|
||||||
if(publicUrl.origin !== window.location.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
|
||||||
|
|
||||||
if(isLocalhost) {
|
|
||||||
checkValidServiceWorker(swUrl, config);
|
|
||||||
|
|
||||||
navigator.serviceWorker.ready.then(() => {
|
|
||||||
console.log(
|
|
||||||
'This web app is being served cache-first by a service ' +
|
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerValidSW(swUrl: string, config?: Config) {
|
|
||||||
navigator.serviceWorker
|
|
||||||
.register(swUrl)
|
|
||||||
.then(registration => {
|
|
||||||
registration.onupdatefound = () => {
|
|
||||||
const installingWorker = registration.installing;
|
|
||||||
if(installingWorker == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
installingWorker.onstatechange = () => {
|
|
||||||
if(installingWorker.state === 'installed') {
|
|
||||||
if(navigator.serviceWorker.controller) {
|
|
||||||
console.log(
|
|
||||||
'New content is available and will be used when all ' +
|
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
|
||||||
);
|
|
||||||
|
|
||||||
if(config && config.onUpdate) {
|
|
||||||
config.onUpdate(registration);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('Content is cached for offline use.');
|
|
||||||
|
|
||||||
if(config && config.onSuccess) {
|
|
||||||
config.onSuccess(registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
|
||||||
fetch(swUrl)
|
|
||||||
.then(response => {
|
|
||||||
const contentType = response.headers.get('content-type');
|
|
||||||
if(
|
|
||||||
response.status === 404 ||
|
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
|
||||||
) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister().then(() => {
|
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregister() {
|
|
||||||
if('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.unregister();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
// This optional code is used to register a service worker.
|
||||||
|
// register() is not called by default.
|
||||||
|
|
||||||
|
// This lets the app load faster on subsequent visits in production, and gives
|
||||||
|
// it offline capabilities. However, it also means that developers (and users)
|
||||||
|
// will only see deployed updates on subsequent visits to a page, after all the
|
||||||
|
// existing tabs open on the page have been closed, since previously cached
|
||||||
|
// resources are updated in the background.
|
||||||
|
|
||||||
|
// To learn more about the benefits of this model and instructions on how to
|
||||||
|
// opt-in, read https://cra.link/PWA
|
||||||
|
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||||
|
);
|
||||||
|
|
||||||
|
type Config = {
|
||||||
|
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||||
|
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function register(config?: Config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://cra.link/PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl: string, config?: Config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then((registration) => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { 'Service-Worker': 'script' },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log('No internet connection found. App is running in offline mode.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then((registration) => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Regular → Executable
+2
-2
@@ -6,7 +6,7 @@ const initialState = {
|
|||||||
};
|
};
|
||||||
type Action = {
|
type Action = {
|
||||||
type: string
|
type: string
|
||||||
userData: UserType
|
userData: Unmei.UserType
|
||||||
};
|
};
|
||||||
const reducer = (state = initialState, action: Action) => {
|
const reducer = (state = initialState, action: Action) => {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
@@ -26,7 +26,7 @@ const reducer = (state = initialState, action: Action) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setUser = (userData: UserType) => ({
|
const setUser = (userData: Unmei.UserType) => ({
|
||||||
type: SET_USER,
|
type: SET_USER,
|
||||||
userData
|
userData
|
||||||
});
|
});
|
||||||
|
|||||||
Regular → Executable
+2
-1
@@ -4,7 +4,8 @@ import modal from "./modal";
|
|||||||
import notifications from './notifications'
|
import notifications from './notifications'
|
||||||
import userSettings from './userSettings';
|
import userSettings from './userSettings';
|
||||||
import snowfall from './snowfall';
|
import snowfall from './snowfall';
|
||||||
|
import preloader from './preloader';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
currentUser, modal, notifications, userSettings, snowfall
|
currentUser, modal, notifications, userSettings, snowfall, preloader
|
||||||
});
|
});
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
@@ -0,0 +1,26 @@
|
|||||||
|
// Actions
|
||||||
|
const SET_PRELOADER_STATUS = 'unmei/notifications/SET_PRELOADER_STATUS';
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
const initialState = false;
|
||||||
|
type Action = {
|
||||||
|
type: string
|
||||||
|
status: boolean
|
||||||
|
};
|
||||||
|
const reducer = (state = initialState, action: Action) => {
|
||||||
|
switch(action.type) {
|
||||||
|
case SET_PRELOADER_STATUS:
|
||||||
|
return action.status;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions creator
|
||||||
|
const setPreloader = (status: boolean) => ({
|
||||||
|
type: SET_PRELOADER_STATUS,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
|
||||||
|
export default reducer;
|
||||||
|
export { setPreloader };
|
||||||
Regular → Executable
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
const SET_SETTINGS = 'unmei/userSettings/SET_SETTINGS';
|
const SET_SETTINGS = 'unmei/userSettings/SET_SETTINGS';
|
||||||
|
|
||||||
const reducer = (state = {}, action: Action<{ settings: UserSettingsType }>) => {
|
const reducer = (state = {}, action: Action<{ settings: Unmei.UserSettingsType }>) => {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
case SET_SETTINGS:
|
case SET_SETTINGS:
|
||||||
state = Object.assign({}, state, action.payload.settings);
|
state = Object.assign({}, state, action.payload.settings);
|
||||||
@@ -10,7 +10,7 @@ const reducer = (state = {}, action: Action<{ settings: UserSettingsType }>) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setSettings = (settings: UserSettingsType) => ({
|
const setSettings = (settings: Unmei.UserSettingsType) => ({
|
||||||
type: SET_SETTINGS,
|
type: SET_SETTINGS,
|
||||||
payload: { settings }
|
payload: { settings }
|
||||||
});
|
});
|
||||||
|
|||||||
Regular → Executable
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
import { createStore } from "redux";
|
import { createStore } from "redux";
|
||||||
import { devToolsEnhancer } from "redux-devtools-extension";
|
import { composeWithDevTools } from "redux-devtools-extension";
|
||||||
import reducers from "./ducks/index";
|
import reducers from "./ducks/index";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
const composeEnhancers = devToolsEnhancer({ trace: true });
|
const composeEnhancers = composeWithDevTools({ trace: true });
|
||||||
const sentryReduxEnhancer = Sentry.createReduxEnhancer({});
|
const sentryReduxEnhancer = Sentry.createReduxEnhancer({});
|
||||||
|
|
||||||
const store = process.env.NODE_ENV === 'development'
|
const store = process.env.NODE_ENV === 'development'
|
||||||
? createStore(reducers, composeEnhancers)
|
? createStore(reducers, composeEnhancers())
|
||||||
: createStore(reducers, sentryReduxEnhancer);
|
: createStore(reducers, sentryReduxEnhancer);
|
||||||
export default store;
|
export default store;
|
||||||
|
|||||||
+27
-25
@@ -1,31 +1,33 @@
|
|||||||
type Theme = 'dark' | 'blue' | 'red' | 'green' | 'light' | 'custom' | 'win95';
|
declare module Unmei {
|
||||||
type ExitStatus = 'came_out';
|
type Theme = 'dark' | 'blue' | 'red' | 'green' | 'light' | 'custom' | 'win95';
|
||||||
type NovelPlatform = 'win';
|
type ExitStatus = 'in_developing' | 'came_out' | 'canceled' | 'announcement' | 'postponed';
|
||||||
|
type NovelPlatform = 'win' | 'mac' | 'linux' | 'ios' | 'android' | 'ps4' | 'ps3' | 'ps2' | 'ps' | 'psp';
|
||||||
|
|
||||||
type ApiError = {
|
type ApiError = {
|
||||||
code: number
|
code: number
|
||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Pagination = {
|
type Pagination = {
|
||||||
offset?: number
|
offset?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
total?: number
|
total?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiResponse<T> = {
|
type ApiResponse<T> = {
|
||||||
error: boolean
|
error: boolean
|
||||||
error_data?: ApiError | string
|
error_data?: ApiError | string
|
||||||
pagination?: Pagination
|
pagination?: Pagination
|
||||||
data?: T
|
data?: T
|
||||||
}
|
}
|
||||||
|
|
||||||
type VersionResponse = {
|
type VersionResponse = {
|
||||||
version: string
|
version: string
|
||||||
build: number
|
build: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommentsResponse = {
|
type CommentsResponse = {
|
||||||
comments: CommentType[]
|
comments: Unmei.CommentType[]
|
||||||
count: number
|
count: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -4,11 +4,12 @@ type Snowfall = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StoreState = {
|
type StoreState = {
|
||||||
currentUser: UserType
|
currentUser: Unmei.UserType
|
||||||
userSettings: UserSettingsType
|
userSettings: Unmei.UserSettingsType
|
||||||
notifications: React.ReactNode[]
|
notifications: React.ReactNode[]
|
||||||
modal: React.ReactNode
|
modal: React.ReactNode
|
||||||
snowfall: Snowfall
|
snowfall: Snowfall
|
||||||
|
preloader: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action<T> = {
|
type Action<T> = {
|
||||||
@@ -16,11 +17,12 @@ type Action<T> = {
|
|||||||
payload: T
|
payload: T
|
||||||
}
|
}
|
||||||
|
|
||||||
type SetUser = (user: UserType) => void
|
type SetUser = (user: Unmei.UserType) => void
|
||||||
type LogoutUser = () => void
|
type LogoutUser = () => void
|
||||||
type SetSettings = (settings: UserSettingsType) => void
|
type SetSettings = (settings: Unmei.UserSettingsType) => void
|
||||||
type SetModal = (modal: React.ReactNode | null) => void
|
type SetModal = (modal: React.ReactNode | null) => void
|
||||||
type HideModal = () => void
|
type HideModal = () => void
|
||||||
type AddNotification = (notification: React.ReactNode) => void
|
type AddNotification = (notification: React.ReactNode) => void
|
||||||
type SetSnowflakeCount = (snowflakeCount: number) => void
|
type SetSnowflakeCount = (snowflakeCount: number) => void
|
||||||
type SetSnowflakeStatus = (snowfallStatus: boolean) => void
|
type SetSnowflakeStatus = (snowfallStatus: boolean) => void
|
||||||
|
type SetPreloaderStatus = (status: boolean) => void
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user