feat: testing mode banner + snapshot storage fields fix
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "white-nights",
|
"name": "white-nights",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.5",
|
"version": "1.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import { CustomTheme } from "@shared";
|
|||||||
import { ThemeProvider } from "@mui/material/styles";
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import { GlobalErrorBoundary } from "./GlobalErrorBoundary";
|
import { GlobalErrorBoundary } from "./GlobalErrorBoundary";
|
||||||
|
import { TestingModeBanner } from "@widgets";
|
||||||
|
|
||||||
export const App: React.FC = () => (
|
export const App: React.FC = () => (
|
||||||
<GlobalErrorBoundary>
|
<GlobalErrorBoundary>
|
||||||
<ThemeProvider theme={CustomTheme.Light}>
|
<ThemeProvider theme={CustomTheme.Light}>
|
||||||
|
<TestingModeBanner />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<Router />
|
<Router />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
export const PreviewLeftWidget = observer(() => {
|
export const PreviewLeftWidget = observer(() => {
|
||||||
const { articleMedia, articleData } = articlesStore;
|
const { articleMedia, articleData } = articlesStore;
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
|
const body = articleData?.[language]?.body;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
@@ -66,7 +67,7 @@ export const PreviewLeftWidget = observer(() => {
|
|||||||
{articleData?.[language]?.heading || "Название информации"}
|
{articleData?.[language]?.heading || "Название информации"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{articleData?.[language]?.body && (
|
{body && (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
padding: 1,
|
padding: 1,
|
||||||
@@ -77,7 +78,7 @@ export const PreviewLeftWidget = observer(() => {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ReactMarkdownComponent value={articleData?.[language]?.body} />
|
<ReactMarkdownComponent value={body} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -52,6 +51,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
const [turn, setTurn] = useState("");
|
const [turn, setTurn] = useState("");
|
||||||
const [centerLat, setCenterLat] = useState("");
|
const [centerLat, setCenterLat] = useState("");
|
||||||
const [centerLng, setCenterLng] = useState("");
|
const [centerLng, setCenterLng] = useState("");
|
||||||
|
const [videoTimer, setVideoTimer] = useState(60);
|
||||||
const [videoPreview, setVideoPreview] = useState<string>("");
|
const [videoPreview, setVideoPreview] = useState<string>("");
|
||||||
const [icon, setIcon] = useState<string>("");
|
const [icon, setIcon] = useState<string>("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -279,6 +279,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
path,
|
path,
|
||||||
video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined,
|
video_preview: !isMediaIdEmpty(videoPreview) ? videoPreview : undefined,
|
||||||
icon: !isMediaIdEmpty(icon) ? icon : undefined,
|
icon: !isMediaIdEmpty(icon) ? icon : undefined,
|
||||||
|
video_timer: videoTimer,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (governor_appeal !== undefined) {
|
if (governor_appeal !== undefined) {
|
||||||
@@ -301,7 +302,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<div className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -375,6 +376,7 @@ export const RouteCreatePage = observer(() => {
|
|||||||
}
|
}
|
||||||
placeholder="55.7558 37.6173 55.7539 37.6208"
|
placeholder="55.7558 37.6173 55.7539 37.6208"
|
||||||
sx={{
|
sx={{
|
||||||
|
mt: 1,
|
||||||
"& .MuiInputBase-root": {
|
"& .MuiInputBase-root": {
|
||||||
maxHeight: "500px",
|
maxHeight: "500px",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
@@ -383,7 +385,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
lineHeight: "1.2",
|
lineHeight: "1.2",
|
||||||
padding: "8px 12px",
|
|
||||||
},
|
},
|
||||||
"& .MuiFormHelperText-root": {
|
"& .MuiFormHelperText-root": {
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
@@ -552,6 +553,18 @@ export const RouteCreatePage = observer(() => {
|
|||||||
value={centerLng}
|
value={centerLng}
|
||||||
onChange={(e) => setCenterLng(e.target.value)}
|
onChange={(e) => setCenterLng(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Таймер видео (сек)"
|
||||||
|
type="number"
|
||||||
|
value={videoTimer}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Math.max(1, Math.round(Number(e.target.value)));
|
||||||
|
if (Number.isFinite(val)) {
|
||||||
|
setVideoTimer(val);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -641,6 +654,6 @@ export const RouteCreatePage = observer(() => {
|
|||||||
onClose={() => setIsPreviewIconOpen(false)}
|
onClose={() => setIsPreviewIconOpen(false)}
|
||||||
mediaId={previewIconId}
|
mediaId={previewIconId}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Select,
|
Select,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -293,7 +292,7 @@ export const RouteEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper className="w-full h-full p-3 flex flex-col gap-10">
|
<div className="w-full h-full p-3 flex flex-col gap-10">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -403,6 +402,7 @@ export const RouteEditPage = observer(() => {
|
|||||||
}
|
}
|
||||||
placeholder="55.7558 37.6173 55.7539 37.6208"
|
placeholder="55.7558 37.6173 55.7539 37.6208"
|
||||||
sx={{
|
sx={{
|
||||||
|
mt: 1,
|
||||||
"& .MuiInputBase-root": {
|
"& .MuiInputBase-root": {
|
||||||
maxHeight: "500px",
|
maxHeight: "500px",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
@@ -411,7 +411,6 @@ export const RouteEditPage = observer(() => {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
lineHeight: "1.2",
|
lineHeight: "1.2",
|
||||||
padding: "8px 12px",
|
|
||||||
},
|
},
|
||||||
"& .MuiFormHelperText-root": {
|
"& .MuiFormHelperText-root": {
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
@@ -547,6 +546,18 @@ export const RouteEditPage = observer(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<TextField
|
||||||
|
className="w-full"
|
||||||
|
label="Таймер видео (сек)"
|
||||||
|
type="number"
|
||||||
|
value={editRouteData.video_timer ?? 60}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = Math.max(1, Math.round(Number(e.target.value)));
|
||||||
|
if (Number.isFinite(val)) {
|
||||||
|
routeStore.setEditRouteData({ video_timer: val });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
||||||
Обращение к пассажирам
|
Обращение к пассажирам
|
||||||
@@ -743,6 +754,6 @@ export const RouteEditPage = observer(() => {
|
|||||||
onCancel={() => setIsDeleteIconModalOpen(false)}
|
onCancel={() => setIsDeleteIconModalOpen(false)}
|
||||||
edit
|
edit
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export const SnapshotListPage = observer(() => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
const availableGB = storageInfo ? storageInfo.available_memory : null;
|
const availableGB = storageInfo ? storageInfo.available_disk_space_gb : null;
|
||||||
const totalGB = storageInfo ? storageInfo.all_memory : null;
|
const totalGB = storageInfo ? storageInfo.total_disk_space_gb : null;
|
||||||
const usedGB =
|
const usedGB =
|
||||||
totalGB !== null && availableGB !== null ? totalGB - availableGB : null;
|
totalGB !== null && availableGB !== null ? totalGB - availableGB : null;
|
||||||
const isLowStorage =
|
const isLowStorage =
|
||||||
@@ -91,7 +91,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
field: "occupied_memory",
|
field: "occupied_disk_space_gb",
|
||||||
headerName: "Размер",
|
headerName: "Размер",
|
||||||
width: 120,
|
width: 120,
|
||||||
renderCell: (params: GridRenderCellParams) => {
|
renderCell: (params: GridRenderCellParams) => {
|
||||||
@@ -151,12 +151,12 @@ export const SnapshotListPage = observer(() => {
|
|||||||
name: snapshot.Name,
|
name: snapshot.Name,
|
||||||
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
||||||
created_at: formatCreationTime(snapshot.CreationTime),
|
created_at: formatCreationTime(snapshot.CreationTime),
|
||||||
occupied_memory: snapshot.occupied_memory,
|
occupied_disk_space_gb: snapshot.occupied_disk_space_gb,
|
||||||
}));
|
}));
|
||||||
}, [snapshots, searchQuery]);
|
}, [snapshots, searchQuery]);
|
||||||
|
|
||||||
const snapshotsGB = rows.reduce(
|
const snapshotsGB = rows.reduce(
|
||||||
(sum, row) => sum + (row.occupied_memory ?? 0),
|
(sum, row) => sum + (row.occupied_disk_space_gb ?? 0),
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const systemGB = usedGB !== null ? Math.max(0, usedGB - snapshotsGB) : null;
|
const systemGB = usedGB !== null ? Math.max(0, usedGB - snapshotsGB) : null;
|
||||||
@@ -175,7 +175,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{usedGB !== null && totalGB !== null && (
|
{usedGB != null && totalGB != null && (
|
||||||
<div className="bg-white rounded-2xl p-5 mb-6 shadow-sm border border-gray-100">
|
<div className="bg-white rounded-2xl p-5 mb-6 shadow-sm border border-gray-100">
|
||||||
<div className="flex items-baseline gap-3 mb-3">
|
<div className="flex items-baseline gap-3 mb-3">
|
||||||
<span className="text-lg font-semibold">Хранилище</span>
|
<span className="text-lg font-semibold">Хранилище</span>
|
||||||
@@ -187,8 +187,8 @@ export const SnapshotListPage = observer(() => {
|
|||||||
<div className="flex w-full h-3 rounded-lg overflow-hidden bg-gray-100">
|
<div className="flex w-full h-3 rounded-lg overflow-hidden bg-gray-100">
|
||||||
{rows.map((row, i) => {
|
{rows.map((row, i) => {
|
||||||
const pct =
|
const pct =
|
||||||
row.occupied_memory != null && totalGB > 0
|
row.occupied_disk_space_gb != null && totalGB > 0
|
||||||
? (row.occupied_memory / totalGB) * 100
|
? (row.occupied_disk_space_gb / totalGB) * 100
|
||||||
: 0;
|
: 0;
|
||||||
if (pct <= 0) return null;
|
if (pct <= 0) return null;
|
||||||
return (
|
return (
|
||||||
@@ -199,7 +199,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
||||||
}}
|
}}
|
||||||
title={`${row.name}: ${row.occupied_memory?.toFixed(1)} ГБ`}
|
title={`${row.name}: ${row.occupied_disk_space_gb?.toFixed(1)} ГБ`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -216,7 +216,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
|
|
||||||
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3">
|
<div className="flex flex-wrap gap-x-5 gap-y-1 mt-3">
|
||||||
{rows.map((row, i) => {
|
{rows.map((row, i) => {
|
||||||
if (row.occupied_memory == null || row.occupied_memory <= 0)
|
if (row.occupied_disk_space_gb == null || row.occupied_disk_space_gb <= 0)
|
||||||
return null;
|
return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type SightCommonInfo = {
|
|||||||
left_article: number;
|
left_article: number;
|
||||||
preview_media: string | null;
|
preview_media: string | null;
|
||||||
video_preview: string | null;
|
video_preview: string | null;
|
||||||
|
preview_font_size?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SightBaseInfo = {
|
export type SightBaseInfo = {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type Route = {
|
|||||||
scale_max: number;
|
scale_max: number;
|
||||||
scale_min: number;
|
scale_min: number;
|
||||||
video_preview: string;
|
video_preview: string;
|
||||||
|
video_timer: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RouteStore {
|
class RouteStore {
|
||||||
@@ -150,6 +151,7 @@ class RouteStore {
|
|||||||
scale_max: 0,
|
scale_max: 0,
|
||||||
scale_min: 0,
|
scale_min: 0,
|
||||||
video_preview: "" as string | undefined,
|
video_preview: "" as string | undefined,
|
||||||
|
video_timer: 60,
|
||||||
};
|
};
|
||||||
|
|
||||||
setEditRouteData = (data: any) => {
|
setEditRouteData = (data: any) => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Snapshot = {
|
|||||||
Name: string;
|
Name: string;
|
||||||
ParentID: string;
|
ParentID: string;
|
||||||
CreationTime: string;
|
CreationTime: string;
|
||||||
occupied_memory: number;
|
occupied_disk_space_gb: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type SnapshotStatus = {
|
type SnapshotStatus = {
|
||||||
@@ -34,8 +34,8 @@ type SnapshotStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StorageInfo = {
|
type StorageInfo = {
|
||||||
available_memory: number;
|
available_disk_space_gb: number;
|
||||||
all_memory: number;
|
total_disk_space_gb: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SnapshotStore {
|
class SnapshotStore {
|
||||||
@@ -271,10 +271,10 @@ class SnapshotStore {
|
|||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.snapshots = this.snapshots.filter((s) => s.ID !== id);
|
this.snapshots = this.snapshots.filter((s) => s.ID !== id);
|
||||||
if (this.storageInfo && snapshot?.occupied_memory) {
|
if (this.storageInfo && snapshot?.occupied_disk_space_gb) {
|
||||||
this.storageInfo = {
|
this.storageInfo = {
|
||||||
...this.storageInfo,
|
...this.storageInfo,
|
||||||
available_memory: this.storageInfo.available_memory + snapshot.occupied_memory,
|
available_disk_space_gb: this.storageInfo.available_disk_space_gb + snapshot.occupied_disk_space_gb,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
18
src/shared/store/TestingModeStore/api.ts
Normal file
18
src/shared/store/TestingModeStore/api.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { authInstance } from "@shared";
|
||||||
|
|
||||||
|
export type TestingModeResponse = {
|
||||||
|
enabled: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTestingModeApi = async (): Promise<TestingModeResponse> => {
|
||||||
|
const response = await authInstance.get("/testing-mode");
|
||||||
|
return response.data as TestingModeResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setTestingModeApi = async (request: {
|
||||||
|
enabled: boolean;
|
||||||
|
}): Promise<TestingModeResponse> => {
|
||||||
|
const response = await authInstance.post("/testing-mode", request);
|
||||||
|
return response.data as TestingModeResponse;
|
||||||
|
};
|
||||||
62
src/shared/store/TestingModeStore/index.ts
Normal file
62
src/shared/store/TestingModeStore/index.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { makeAutoObservable } from "mobx";
|
||||||
|
import { mobxFetch } from "@shared";
|
||||||
|
import {
|
||||||
|
TestingModeResponse,
|
||||||
|
getTestingModeApi,
|
||||||
|
setTestingModeApi,
|
||||||
|
} from "./api";
|
||||||
|
|
||||||
|
const POLLING_INTERVAL = 10_000;
|
||||||
|
|
||||||
|
class TestingModeStore {
|
||||||
|
testingMode: TestingModeResponse | null = null;
|
||||||
|
testingModeLoading = false;
|
||||||
|
testingModeError: string | null = null;
|
||||||
|
|
||||||
|
setTestingModeResult: TestingModeResponse | null = null;
|
||||||
|
setTestingModeLoading = false;
|
||||||
|
setTestingModeError: string | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isEnabled(): boolean {
|
||||||
|
return this.testingMode?.enabled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTestingModeAction = mobxFetch<TestingModeResponse, TestingModeStore>({
|
||||||
|
store: this,
|
||||||
|
value: "testingMode",
|
||||||
|
loading: "testingModeLoading",
|
||||||
|
error: "testingModeError",
|
||||||
|
fn: getTestingModeApi,
|
||||||
|
pollingInterval: POLLING_INTERVAL,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTestingModeAction = mobxFetch<
|
||||||
|
{ enabled: boolean },
|
||||||
|
TestingModeResponse,
|
||||||
|
TestingModeStore
|
||||||
|
>({
|
||||||
|
store: this,
|
||||||
|
value: "setTestingModeResult",
|
||||||
|
loading: "setTestingModeLoading",
|
||||||
|
error: "setTestingModeError",
|
||||||
|
fn: setTestingModeApi,
|
||||||
|
onSuccess: (result) => {
|
||||||
|
this.testingMode = result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
startPolling() {
|
||||||
|
this.fetchTestingModeAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
stopPolling() {
|
||||||
|
this.fetchTestingModeAction.stopPolling?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testingModeStore = new TestingModeStore();
|
||||||
|
export { TestingModeStore };
|
||||||
@@ -16,3 +16,4 @@ export * from "./CarrierStore";
|
|||||||
export * from "./StationsStore";
|
export * from "./StationsStore";
|
||||||
export * from "./MenuStore";
|
export * from "./MenuStore";
|
||||||
export * from "./SelectedCityStore";
|
export * from "./SelectedCityStore";
|
||||||
|
export * from "./TestingModeStore";
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface SightFramePreviewProps {
|
|||||||
previewMedia: PreviewMediaData | null;
|
previewMedia: PreviewMediaData | null;
|
||||||
articles: Article[];
|
articles: Article[];
|
||||||
onArticleSelect: (index: number) => void;
|
onArticleSelect: (index: number) => void;
|
||||||
|
previewFontSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches SightFrame.jsx renderCurrentMedia — same structure, same CSS classes
|
// Matches SightFrame.jsx renderCurrentMedia — same structure, same CSS classes
|
||||||
@@ -151,6 +152,7 @@ export function SightFramePreview({
|
|||||||
previewMedia,
|
previewMedia,
|
||||||
articles,
|
articles,
|
||||||
onArticleSelect,
|
onArticleSelect,
|
||||||
|
previewFontSize,
|
||||||
}: SightFramePreviewProps) {
|
}: SightFramePreviewProps) {
|
||||||
const token = localStorage.getItem("token") ?? "";
|
const token = localStorage.getItem("token") ?? "";
|
||||||
|
|
||||||
@@ -235,7 +237,12 @@ export function SightFramePreview({
|
|||||||
{/* title: intro-title (300px, 40px centered) or regular (24px with border) */}
|
{/* title: intro-title (300px, 40px centered) or regular (24px with border) */}
|
||||||
<div
|
<div
|
||||||
className={`sfp-sight-frame-title${isIntro ? " sfp-intro-title" : ""}`}
|
className={`sfp-sight-frame-title${isIntro ? " sfp-intro-title" : ""}`}
|
||||||
style={{ lineHeight: isIntro ? undefined : titleLineHeight }}
|
style={{
|
||||||
|
lineHeight: isIntro ? undefined : titleLineHeight,
|
||||||
|
...(isIntro && previewFontSize != null
|
||||||
|
? { fontSize: `${previewFontSize}px` }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
|
Slider,
|
||||||
|
Stack,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
authInstance,
|
authInstance,
|
||||||
@@ -45,6 +47,7 @@ export const RightWidgetTab = observer(
|
|||||||
updateRightArticleInfo,
|
updateRightArticleInfo,
|
||||||
getRightArticles,
|
getRightArticles,
|
||||||
updateSight,
|
updateSight,
|
||||||
|
updateSightInfo,
|
||||||
unlinkPreviewMedia,
|
unlinkPreviewMedia,
|
||||||
linkPreviewMedia,
|
linkPreviewMedia,
|
||||||
unlinkRightArticle,
|
unlinkRightArticle,
|
||||||
@@ -454,7 +457,40 @@ export const RightWidgetTab = observer(
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ flexShrink: 0, width: "550px" }}>
|
<Box sx={{ flexShrink: 0, width: "550px", display: "flex", flexDirection: "column", gap: 1 }}>
|
||||||
|
<Stack direction="row" spacing={2} alignItems="center">
|
||||||
|
<TextField
|
||||||
|
type="number"
|
||||||
|
label="Размер шрифта превью (px)"
|
||||||
|
size="small"
|
||||||
|
value={sight.common.preview_font_size ?? ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
if (raw === "") {
|
||||||
|
updateSightInfo(language, { preview_font_size: undefined }, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const val = Math.max(1, Math.min(300, Math.round(Number(raw))));
|
||||||
|
if (Number.isFinite(val)) {
|
||||||
|
updateSightInfo(language, { preview_font_size: val }, true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
slotProps={{ input: { min: 1, max: 300 } }}
|
||||||
|
sx={{ width: "200px" }}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
value={sight.common.preview_font_size ?? 40}
|
||||||
|
min={1}
|
||||||
|
max={300}
|
||||||
|
step={1}
|
||||||
|
onChange={(_, newValue) => {
|
||||||
|
if (typeof newValue === "number") {
|
||||||
|
updateSightInfo(language, { preview_font_size: newValue }, true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
<SightFramePreview
|
<SightFramePreview
|
||||||
sightName={sight[language].name}
|
sightName={sight[language].name}
|
||||||
previewMedia={previewMedia}
|
previewMedia={previewMedia}
|
||||||
@@ -463,6 +499,7 @@ export const RightWidgetTab = observer(
|
|||||||
handleSelectArticle(idx);
|
handleSelectArticle(idx);
|
||||||
setType("article");
|
setType("article");
|
||||||
}}
|
}}
|
||||||
|
previewFontSize={sight.common.preview_font_size}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
43
src/widgets/TestingModeBanner/index.tsx
Normal file
43
src/widgets/TestingModeBanner/index.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { testingModeStore, authStore } from "@shared";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export const TestingModeBanner = observer(() => {
|
||||||
|
const { isAuthenticated } = authStore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
testingModeStore.stopPolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
testingModeStore.startPolling();
|
||||||
|
return () => {
|
||||||
|
testingModeStore.stopPolling();
|
||||||
|
};
|
||||||
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
|
if (!testingModeStore.isEnabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 2147483647,
|
||||||
|
backgroundColor: "#d32f2f",
|
||||||
|
color: "#fff",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "12px 16px",
|
||||||
|
fontWeight: 700,
|
||||||
|
fontSize: "14px",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.4)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ПРОВОДИТСЯ ТЕСТИРОВАНИЕ. ПРОСЬБА НЕ ВЗАИМОДЕЙСТВОВАТЬ С АДМИН-ПАНЕЛЬЮ
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -20,3 +20,4 @@ export * from "./CreateButton";
|
|||||||
export * from "./SaveWithoutCityAgree";
|
export * from "./SaveWithoutCityAgree";
|
||||||
export * from "./CitySelector";
|
export * from "./CitySelector";
|
||||||
export * from "./modals";
|
export * from "./modals";
|
||||||
|
export * from "./TestingModeBanner";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user