feat: Add translation on 3 languages for sight page
This commit is contained in:
@@ -1,16 +1,28 @@
|
||||
import { languageStore, Language } from "@shared";
|
||||
import axios from "axios";
|
||||
import axios, { AxiosError, InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
const authInstance = axios.create({
|
||||
baseURL: "https://wn.krbl.ru",
|
||||
});
|
||||
|
||||
authInstance.interceptors.request.use((config) => {
|
||||
authInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
console.log(config);
|
||||
config.headers.Authorization = `Bearer ${localStorage.getItem("token")}`;
|
||||
config.headers["X-Language"] = languageStore.language ?? "ru";
|
||||
return config;
|
||||
});
|
||||
|
||||
authInstance.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
window.location.href = "/login";
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
const languageInstance = (language: Language) => {
|
||||
const instance = axios.create({
|
||||
baseURL: "https://wn.krbl.ru",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { authStore } from "@shared";
|
||||
import { Power, LucideIcon, Building2, MonitorSmartphone } from "lucide-react";
|
||||
export const DRAWER_WIDTH = 300;
|
||||
|
||||
@@ -5,7 +6,8 @@ interface NavigationItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
path: string;
|
||||
path?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const NAVIGATION_ITEMS: {
|
||||
@@ -31,7 +33,9 @@ export const NAVIGATION_ITEMS: {
|
||||
id: "logout",
|
||||
label: "Выйти",
|
||||
icon: Power,
|
||||
path: "/logout",
|
||||
onClick: () => {
|
||||
authStore.logout();
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import {
|
||||
articlesStore,
|
||||
authStore,
|
||||
Language,
|
||||
mediaStore,
|
||||
MEDIA_TYPE_LABELS,
|
||||
API_URL,
|
||||
} from "@shared";
|
||||
import { mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@@ -17,13 +10,12 @@ import {
|
||||
TextField,
|
||||
Paper,
|
||||
Box,
|
||||
Typography,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Snackbar,
|
||||
} from "@mui/material";
|
||||
import { Download, Save } from "lucide-react";
|
||||
import { ReactMarkdownComponent, MediaViewer } from "@widgets";
|
||||
import { MediaViewer } from "@widgets";
|
||||
import { authInstance } from "@shared";
|
||||
|
||||
interface PreviewMediaDialogProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { articlesStore, authStore, Language } from "@shared";
|
||||
import { articlesStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
@@ -73,7 +73,10 @@ export const SelectArticleModal = observer(
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await Promise.all([getArticle(articleId), getArticleMedia(articleId)]);
|
||||
await Promise.all([
|
||||
getArticle(Number(articleId)),
|
||||
getArticleMedia(Number(articleId)),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch article data:", error);
|
||||
// Reset article data on error
|
||||
@@ -83,9 +86,11 @@ export const SelectArticleModal = observer(
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const filteredArticles = articles
|
||||
// @ts-ignore
|
||||
.filter((article) => !linkedArticleIds.includes(article.id))
|
||||
// @ts-ignore
|
||||
.filter((article) =>
|
||||
article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
@@ -140,6 +145,7 @@ export const SelectArticleModal = observer(
|
||||
{searchQuery ? "Статьи не найдены" : "Нет доступных статей"}
|
||||
</Typography>
|
||||
) : (
|
||||
// @ts-ignore
|
||||
filteredArticles.map((article) => (
|
||||
<ListItemButton
|
||||
key={article.id}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { articlesStore, authStore, Language, mediaStore } from "@shared";
|
||||
import { mediaStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
@@ -12,12 +12,11 @@ import {
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Box,
|
||||
Typography,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import { ImagePlus, Search } from "lucide-react";
|
||||
import { ReactMarkdownComponent, MediaViewer } from "@widgets";
|
||||
import { Search } from "lucide-react";
|
||||
import { MediaViewer } from "@widgets";
|
||||
|
||||
interface SelectMediaDialogProps {
|
||||
open: boolean; // Corrected prop name
|
||||
|
||||
@@ -53,7 +53,7 @@ class ArticlesStore {
|
||||
const response = await authInstance.get(`/sight/${id}/article`);
|
||||
|
||||
runInAction(() => {
|
||||
editSightStore.sightInfo[languageStore.language].right = response.data;
|
||||
editSightStore.sight[languageStore.language].right = response.data;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -66,10 +66,14 @@ class ArticlesStore {
|
||||
};
|
||||
|
||||
getArticleByArticleId = computed(() => {
|
||||
if (editSightStore.sightInfo.left_article) {
|
||||
return this.articles[languageStore.language].find(
|
||||
(a) => a.id == editSightStore.sightInfo.left_article
|
||||
if (editSightStore.sight.common.left_article) {
|
||||
const language = languageStore.language;
|
||||
const foundArticle = this.articles[language].find(
|
||||
(a) => a.id == editSightStore.sight.common.left_article
|
||||
);
|
||||
editSightStore.sight[language].left.heading = foundArticle?.heading ?? "";
|
||||
editSightStore.sight[language].left.body = foundArticle?.body ?? "";
|
||||
return foundArticle;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -79,7 +79,7 @@ class AuthStore {
|
||||
};
|
||||
|
||||
get isAuthenticated() {
|
||||
return this.payload?.token !== null;
|
||||
return this.payload !== null;
|
||||
}
|
||||
|
||||
get user() {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// @shared/stores/editSightStore.ts
|
||||
import { Language } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { authInstance, Language } from "@shared";
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
export interface MediaObject {
|
||||
id: string;
|
||||
filename: string;
|
||||
media_type: number;
|
||||
}
|
||||
|
||||
type SightBaseInfo = {
|
||||
export type SightLanguageInfo = {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
left: { heading: string; body: string };
|
||||
right: { heading: string; body: string }[];
|
||||
};
|
||||
|
||||
export type SightCommonInfo = {
|
||||
city_id: number;
|
||||
city: string;
|
||||
latitude: number;
|
||||
@@ -22,103 +23,14 @@ type SightBaseInfo = {
|
||||
video_preview: string;
|
||||
};
|
||||
|
||||
export interface RightArticleBlock {
|
||||
id: string;
|
||||
type: "article" | "preview_media";
|
||||
name: string;
|
||||
linkedArticleId?: string;
|
||||
heading: string;
|
||||
body: string;
|
||||
media: MediaObject | null;
|
||||
}
|
||||
|
||||
type SightInfo = SightBaseInfo & {
|
||||
[key in Language]: {
|
||||
info: {
|
||||
name: string;
|
||||
address: string;
|
||||
};
|
||||
left: {
|
||||
loaded: boolean; // Означает, что данные для этого языка были инициализированы/загружены
|
||||
heading: string;
|
||||
body: string;
|
||||
media: MediaObject | null;
|
||||
};
|
||||
right: RightArticleBlock[];
|
||||
};
|
||||
export type SightBaseInfo = {
|
||||
common: SightCommonInfo;
|
||||
} & {
|
||||
[key in Language]: SightLanguageInfo;
|
||||
};
|
||||
|
||||
class EditSightStore {
|
||||
sightInfo: SightInfo = {
|
||||
id: 0,
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: "",
|
||||
watermark_lu: "",
|
||||
watermark_rd: "",
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
ru: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
right: [],
|
||||
},
|
||||
en: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
right: [],
|
||||
},
|
||||
zh: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
// loadSightInfo: Используется для первоначальной загрузки данных для ЯЗЫКА.
|
||||
// Она устанавливает loaded: true, чтобы в будущем не перезатирать данные.
|
||||
loadSightInfo = (
|
||||
language: Language,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: MediaObject | null
|
||||
) => {
|
||||
// Важно: если данные уже были загружены или изменены, не перезаписывайте их.
|
||||
// Это предотвращает потерю пользовательского ввода при переключении языков.
|
||||
// Если хотите принудительную загрузку, добавьте другой метод или параметр.
|
||||
if (!this.sightInfo[language].left.loaded) {
|
||||
// <--- Только если еще не загружено
|
||||
this.sightInfo[language].left.heading = heading;
|
||||
this.sightInfo[language].left.body = body;
|
||||
this.sightInfo[language].left.media = media;
|
||||
this.sightInfo[language].left.loaded = true; // <--- Устанавливаем loaded только при загрузке
|
||||
}
|
||||
};
|
||||
|
||||
// updateSightInfo: Используется для сохранения ЛЮБЫХ пользовательских изменений.
|
||||
// Она НЕ должна влиять на флаг 'loaded', который управляется 'loadSightInfo'.
|
||||
updateSightInfo = (
|
||||
language: Language,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: MediaObject | null
|
||||
) => {
|
||||
this.sightInfo[language].left.heading = heading;
|
||||
this.sightInfo[language].left.body = body;
|
||||
this.sightInfo[language].left.media = media;
|
||||
// this.sightInfo[language].left.loaded = true; // <-- УДАЛИТЕ эту строку
|
||||
};
|
||||
|
||||
clearSightInfo = () => {
|
||||
this.sightInfo = {
|
||||
id: 0,
|
||||
sight: SightBaseInfo = {
|
||||
common: {
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
@@ -129,23 +41,123 @@ class EditSightStore {
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
},
|
||||
ru: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
en: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
zh: {
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
hasLoadedCommon = false;
|
||||
getSightInfo = async (id: number, language: Language) => {
|
||||
if (this.sight[language].id === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await authInstance.get(`/sight/${id}`);
|
||||
const data = response.data;
|
||||
|
||||
runInAction(() => {
|
||||
// Обновляем языковую часть
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...data,
|
||||
};
|
||||
|
||||
// Только при первом запросе обновляем общую часть
|
||||
if (!this.hasLoadedCommon) {
|
||||
this.sight.common = {
|
||||
...this.sight.common,
|
||||
...data,
|
||||
};
|
||||
this.hasLoadedCommon = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
updateLeftInfo = (language: Language, heading: string, body: string) => {
|
||||
this.sight[language].left.heading = heading;
|
||||
this.sight[language].left.body = body;
|
||||
};
|
||||
|
||||
clearSightInfo = () => {
|
||||
this.sight = {
|
||||
common: {
|
||||
city_id: 0,
|
||||
city: "",
|
||||
latitude: 0,
|
||||
longitude: 0,
|
||||
thumbnail: "",
|
||||
watermark_lu: "",
|
||||
watermark_rd: "",
|
||||
left_article: 0,
|
||||
preview_media: "",
|
||||
video_preview: "",
|
||||
},
|
||||
ru: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
|
||||
en: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
|
||||
zh: {
|
||||
info: { name: "", address: "" },
|
||||
left: { loaded: false, heading: "", body: "", media: null },
|
||||
id: 0,
|
||||
name: "",
|
||||
address: "",
|
||||
left: { heading: "", body: "" },
|
||||
right: [],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
updateSightInfo = (
|
||||
language: Language,
|
||||
content: Partial<SightLanguageInfo | SightCommonInfo>,
|
||||
common: boolean = false
|
||||
) => {
|
||||
if (common) {
|
||||
this.sight.common = {
|
||||
...this.sight.common,
|
||||
...content,
|
||||
};
|
||||
} else {
|
||||
this.sight[language] = {
|
||||
...this.sight[language],
|
||||
...content,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const editSightStore = new EditSightStore();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import {
|
||||
articlesStore,
|
||||
authInstance,
|
||||
languageInstance,
|
||||
languageStore,
|
||||
editSightStore,
|
||||
SightBaseInfo,
|
||||
} from "@shared";
|
||||
import { computed, makeAutoObservable, runInAction } from "mobx";
|
||||
|
||||
@@ -59,48 +58,40 @@ class SightsStore {
|
||||
});
|
||||
};
|
||||
|
||||
getSight = async (id: number) => {
|
||||
const response = await authInstance.get(`/sight/${id}`);
|
||||
// getSight = async (id: number) => {
|
||||
// const response = await authInstance.get(`/sight/${id}`);
|
||||
|
||||
runInAction(() => {
|
||||
this.sight = response.data;
|
||||
editSightStore.sightInfo = {
|
||||
...editSightStore.sightInfo,
|
||||
id: response.data.id,
|
||||
city_id: response.data.city_id,
|
||||
city: response.data.city,
|
||||
latitude: response.data.latitude,
|
||||
longitude: response.data.longitude,
|
||||
thumbnail: response.data.thumbnail,
|
||||
watermark_lu: response.data.watermark_lu,
|
||||
watermark_rd: response.data.watermark_rd,
|
||||
left_article: response.data.left_article,
|
||||
preview_media: response.data.preview_media,
|
||||
video_preview: response.data.video_preview,
|
||||
[languageStore.language]: {
|
||||
info: {
|
||||
name: response.data.name,
|
||||
address: response.data.address,
|
||||
description: response.data.description,
|
||||
},
|
||||
left: {
|
||||
heading: editSightStore.sightInfo[languageStore.language].left
|
||||
.loaded
|
||||
? editSightStore.sightInfo[languageStore.language].left.heading
|
||||
: articlesStore.articles[languageStore.language].find(
|
||||
(article) => article.id === response.data.left_article
|
||||
)?.heading,
|
||||
body: editSightStore.sightInfo[languageStore.language].left.loaded
|
||||
? editSightStore.sightInfo[languageStore.language].left.body
|
||||
: articlesStore.articles[languageStore.language].find(
|
||||
(article) => article.id === response.data.left_article
|
||||
)?.body,
|
||||
},
|
||||
},
|
||||
};
|
||||
console.log(editSightStore.sightInfo);
|
||||
});
|
||||
};
|
||||
// runInAction(() => {
|
||||
// this.sight = response.data;
|
||||
// editSightStore.sightInfo = {
|
||||
// ...editSightStore.sightInfo,
|
||||
// id: response.data.id,
|
||||
// city_id: response.data.city_id,
|
||||
// city: response.data.city,
|
||||
// latitude: response.data.latitude,
|
||||
// longitude: response.data.longitude,
|
||||
// thumbnail: response.data.thumbnail,
|
||||
// watermark_lu: response.data.watermark_lu,
|
||||
// watermark_rd: response.data.watermark_rd,
|
||||
// left_article: response.data.left_article,
|
||||
// preview_media: response.data.preview_media,
|
||||
// video_preview: response.data.video_preview,
|
||||
|
||||
// [languageStore.language]: {
|
||||
// info: {
|
||||
// name: response.data.name,
|
||||
// address: response.data.address,
|
||||
// },
|
||||
// left: {
|
||||
// heading: articlesStore.articles[languageStore.language].find(
|
||||
// (article) => article.id === response.data.left_article
|
||||
// )?.heading,
|
||||
// body: articlesStore.articles[languageStore.language].find(
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
// });
|
||||
// };
|
||||
|
||||
createSightAction = async (
|
||||
city: number,
|
||||
@@ -160,12 +151,32 @@ class SightsStore {
|
||||
language: Language,
|
||||
content: Partial<CreateSight[Language]>
|
||||
) => {
|
||||
runInAction(() => {
|
||||
this.createSight[language] = {
|
||||
...this.createSight[language],
|
||||
this.createSight[language] = {
|
||||
...this.createSight[language],
|
||||
...content,
|
||||
};
|
||||
};
|
||||
|
||||
updateSight = (
|
||||
language: Language,
|
||||
content: Partial<SightBaseInfo[Language]>,
|
||||
common: boolean
|
||||
) => {
|
||||
if (common) {
|
||||
// @ts-ignore
|
||||
this.sight!.common = {
|
||||
// @ts-ignore
|
||||
...this.sight!.common,
|
||||
...content,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.sight![language] = {
|
||||
// @ts-ignore
|
||||
...this.sight![language],
|
||||
...content,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
clearCreateSight = () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const CoordinatesInput = ({
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<TextField
|
||||
label="Координаты"
|
||||
value={inputValue}
|
||||
value={inputValue ?? ""}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user