added more types for media

This commit is contained in:
Илья Куприец 2025-04-27 11:27:22 +03:00
parent abd054b8d4
commit 0d325a3aa6
206 changed files with 20273 additions and 18977 deletions

View File

@ -1,7 +1,7 @@
# This Dockerfile uses `serve` npm package to serve the static files with node process.
# You can find the Dockerfile for nginx in the following link:
# https://github.com/refinedev/dockerfiles/blob/main/vite/Dockerfile.nginx
FROM refinedev/node:18 AS base
FROM refinedev/node:20 AS base
FROM base as deps

5
compose.yaml Normal file
View File

@ -0,0 +1,5 @@
services:
refine:
image: white-nights:latest
ports:
- "3000:3000"

12782
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,7 @@
"@mui/lab": "^6.0.0-beta.14",
"@mui/material": "^6.1.7",
"@mui/x-data-grid": "^7.22.2",
"@photo-sphere-viewer/core": "^5.13.1",
"@photo-sphere-viewer/core": "^5.13.2",
"@react-three/drei": "^10.0.6",
"@react-three/fiber": "^9.1.2",
"@refinedev/cli": "^2.16.21",
@ -43,7 +43,7 @@
"react-i18next": "^15.4.1",
"react-intl": "^7.1.10",
"react-markdown": "^10.1.0",
"react-photo-sphere-viewer": "^6.2.2",
"react-photo-sphere-viewer": "^6.2.3",
"react-router": "^7.0.2",
"react-simple-maps": "^3.0.0",
"react-simplemde-editor": "^5.2.0",

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
import { Refine, Authenticated } from "@refinedev/core";
import { DevtoolsPanel, DevtoolsProvider } from "@refinedev/devtools";
import { RefineKbar, RefineKbarProvider } from "@refinedev/kbar";
import {
ErrorComponent,
@ -12,7 +11,7 @@ import {
import { customDataProvider } from "./providers/data";
import CssBaseline from "@mui/material/CssBaseline";
import GlobalStyles from "@mui/material/GlobalStyles";
import { BrowserRouter, Route, Routes, Outlet } from "react-router";
import { BrowserRouter, Route, Routes, Outlet, HashRouter } from "react-router";
import routerBindings, {
NavigateToResource,
CatchAllNavigate,
@ -75,18 +74,13 @@ import {
} from "./components/ui/Icons";
import SidebarTitle from "./components/ui/SidebarTitle";
import { AdminOnly } from "./components/AdminOnly";
import { Dashboard } from "./preview/widgets/dashboard/Dashboard";
import { QueryClient } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
import { LoadingProvider } from "@mt/utils";
import { RoutePreview } from "./preview/components/route-preview/components/RoutePreview";
const queryClient = new QueryClient();
import { LoadingProvider } from "@mt/utils";
import { KBarProvider, RefineKbar } from "@refinedev/kbar";
function App() {
return (
<QueryClientProvider client={queryClient}>
<LoadingProvider>
<BrowserRouter>
<HashRouter>
<ColorModeContextProvider>
<CssBaseline />
<GlobalStyles styles={{ html: { WebkitFontSmoothing: "auto" } }} />
@ -227,6 +221,7 @@ function App() {
projectId: "Wv044J-t53S3s-PcbJGe",
}}
>
<KBarProvider>
<Routes>
<Route
element={
@ -265,7 +260,6 @@ function App() {
/>
<Route path="show/:id" element={<CountryShow />} />
</Route>
<Route path="dashboard" element={<Dashboard />} />
<Route path="/city">
<Route index element={<CityList />} />
@ -363,7 +357,6 @@ function App() {
}
/>
<Route path="show/:id" element={<RouteShow />} />
<Route path="preview/:id" element={<RoutePreview />} />
</Route>
<Route path="/user">
@ -425,14 +418,15 @@ function App() {
return "Белые ночи";
}}
/>
<RefineKbar />
</KBarProvider>
</Refine>
<DevtoolsPanel />
</DevtoolsProvider>
</RefineSnackbarProvider>
</ColorModeContextProvider>
</BrowserRouter>
</HashRouter>
</LoadingProvider>
</QueryClientProvider>
);
}

View File

@ -6,6 +6,7 @@ import IconButton from "@mui/material/IconButton";
import Stack from "@mui/material/Stack";
import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import { languageStore } from "../../store/LanguageStore";
import {
useGetIdentity,
useList,
@ -21,7 +22,6 @@ import {
Button,
Select,
MenuItem,
InputLabel,
FormControl,
SelectChangeEvent,
} from "@mui/material";
@ -38,6 +38,7 @@ type IUser = {
export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = observer(
({ sticky }) => {
const { city_id, setCityIdAction } = cityStore;
const { language } = languageStore;
const { data: cities } = useList({
resource: "city",
});
@ -97,6 +98,7 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = observer(
// После сохранения меняем язык и возвращаемся на ту же страницу
Cookies.set("lang", lang);
i18n.changeLanguage(lang);
navigate(currentLocation);
return;
@ -149,6 +151,9 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = observer(
value={city_id}
onChange={handleChange}
>
<MenuItem value={String(0)} key={0}>
Все города
</MenuItem>
{cities.data?.map((city) => (
<MenuItem value={String(city.id)} key={city.id}>
{city.name}
@ -157,31 +162,6 @@ export const Header: React.FC<RefineThemedLayoutV2HeaderProps> = observer(
</Select>
)}
</FormControl>
<Stack
direction="row"
spacing={1}
sx={{
backgroundColor: "background.paper",
padding: "4px",
borderRadius: "4px",
}}
>
{["ru", "en", "zh"].map((lang) => (
<Button
key={lang}
onClick={() => handleLanguageChange(lang)}
variant={i18n.language === lang ? "contained" : "outlined"}
size="small"
sx={{
minWidth: "30px",
padding: "2px 0px",
textTransform: "uppercase",
}}
>
{lang}
</Button>
))}
</Stack>
<IconButton
color="inherit"

View File

@ -1,72 +1,142 @@
import {useState} from 'react'
import {UseFormSetError, UseFormClearErrors, UseFormSetValue} from 'react-hook-form'
import { useState } from "react";
import {
UseFormSetError,
UseFormClearErrors,
UseFormSetValue,
} from "react-hook-form";
export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
export const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/ogg']
export const ALLOWED_IMAGE_TYPES = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
];
export const ALLOWED_VIDEO_TYPES = ["video/mp4", "video/webm", "video/ogg"];
export const ALLOWED_PANORAMA_TYPES = [
"image/jpeg",
"image/jpg",
"image/png",
"image/gif",
"image/webp",
];
export const ALLOWED_ICON_TYPES = [
"image/svg+xml",
"image/png",
"image/jpg",
"image/jpeg",
"image/webp",
];
export const ALLOWED_WATERMARK_TYPES = [
"image/svg+xml",
"image/png",
"image/jpg",
"image/jpeg",
"image/webp",
];
export const ALLOWED_3D_MODEL_TYPES = [
".glb",
"glb",
".gltf",
"gltf",
"model/gltf-binary",
".vnd.ms-3d",
];
export const validateFileType = (file: File, mediaType: number) => {
if (mediaType === 1 && !ALLOWED_IMAGE_TYPES.includes(file.type)) {
return 'Для типа "Фото" разрешены только форматы: JPG, PNG, GIF, WEBP'
return 'Для типа "Фото" разрешены только форматы: JPG, PNG, GIF, WEBP';
}
if (mediaType === 2 && !ALLOWED_VIDEO_TYPES.includes(file.type)) {
return 'Для типа "Видео" разрешены только форматы: MP4, WEBM, OGG'
return 'Для типа "Видео" разрешены только форматы: MP4, WEBM, OGG';
}
return null
if (mediaType === 3 && !ALLOWED_ICON_TYPES.includes(file.type)) {
return 'Для типа "Иконка" разрешены только форматы: SVG, PNG, JPG, JPEG, WEBP';
}
if (mediaType === 4 && !ALLOWED_WATERMARK_TYPES.includes(file.type)) {
return 'Для типа "Водяной знак" разрешены только форматы: SVG, PNG, JPG, JPEG, WEBP';
}
if (mediaType === 5 && !ALLOWED_PANORAMA_TYPES.includes(file.type)) {
return 'Для типа "Панорама" разрешены только форматы: JPG, PNG, GIF, WEBP';
}
if (mediaType === 6 && !ALLOWED_3D_MODEL_TYPES.includes(file.type)) {
const extension = file.name.split(".").pop();
const isMimeTypeValid = ["model/gltf-binary"].includes(file.type);
const isExtensionValid =
extension && ALLOWED_3D_MODEL_TYPES.includes(extension);
if (!isMimeTypeValid && !isExtensionValid) {
return 'Для типа "3D-модель" разрешены только форматы: GLB, GLTF';
}
}
return null;
};
type UseMediaFileUploadProps = {
selectedMediaType: number
setError: UseFormSetError<any>
clearErrors: UseFormClearErrors<any>
setValue: UseFormSetValue<any>
}
selectedMediaType: number;
setError: UseFormSetError<any>;
clearErrors: UseFormClearErrors<any>;
setValue: UseFormSetValue<any>;
};
export const useMediaFileUpload = ({selectedMediaType, setError, clearErrors, setValue}: UseMediaFileUploadProps) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
export const useMediaFileUpload = ({
selectedMediaType,
setError,
clearErrors,
setValue,
}: UseMediaFileUploadProps) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
const file = event.target.files?.[0];
if (!file) return;
if (selectedMediaType) {
const error = validateFileType(file, selectedMediaType)
const error = validateFileType(file, selectedMediaType);
if (error) {
setError('file', {type: 'manual', message: error})
event.target.value = ''
return
setError("file", { type: "manual", message: error });
event.target.value = "";
return;
}
}
clearErrors('file')
setValue('file', file)
setSelectedFile(file)
clearErrors("file");
setValue("file", file);
setSelectedFile(file);
if (file.type.startsWith('image/')) {
const url = URL.createObjectURL(file)
setPreviewUrl(url)
if (file.type.startsWith("image/")) {
const url = URL.createObjectURL(file);
setPreviewUrl(url);
} else {
setPreviewUrl(null)
}
setPreviewUrl(null);
}
};
const handleMediaTypeChange = (newMediaType: number | null) => {
setValue('media_type', newMediaType || null)
setValue("media_type", newMediaType || null);
if (selectedFile && newMediaType) {
const error = validateFileType(selectedFile, newMediaType)
const error = validateFileType(selectedFile, newMediaType);
if (error) {
setError('file', {type: 'manual', message: error})
setValue('file', null)
setSelectedFile(null)
setPreviewUrl(null)
setError("file", { type: "manual", message: error });
setValue("file", null);
setSelectedFile(null);
setPreviewUrl(null);
} else {
clearErrors('file')
}
clearErrors("file");
}
}
};
return {
selectedFile,
@ -75,5 +145,5 @@ export const useMediaFileUpload = ({selectedMediaType, setError, clearErrors, se
setPreviewUrl,
handleFileChange,
handleMediaTypeChange,
}
}
};
};

View File

@ -1,6 +1,10 @@
export const MEDIA_TYPES = [
{ label: "Фото", value: 1 },
{ label: "Видео", value: 2 },
{ label: "Иконка", value: 3 },
{ label: "Водяной знак", value: 4 },
{ label: "Панорама", value: 5 },
{ label: "3Д-модель", value: 6 },
];
export const VEHICLE_TYPES = [

View File

@ -22,7 +22,7 @@ export const ArticleEdit = () => {
// useEffect(() => {
// Cookies.set("lang", initialLanguage);
// }, [pathname]);
const [language, setLanguage] = useState(Cookies.get("lang")!);
const [language, setLanguage] = useState(Cookies.get("lang") || "ru");
const [articleData, setArticleData] = useState<{
ru: { heading: string; body: string };
en: { heading: string; body: string };

View File

@ -20,7 +20,7 @@ export const CarrierList = observer(() => {
{
field: "cityID",
operator: "eq",
value: city_id,
value: city_id === "0" ? null : city_id,
},
],
},

View File

@ -1,4 +1,3 @@
import { Icons } from "../../preview/components";
import { Box, TextField } from "@mui/material";
import { Edit } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";

View File

@ -0,0 +1,21 @@
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Stage, useGLTF } from "@react-three/drei";
type ModelViewerProps = {
fileUrl: string;
};
export const ModelViewer = ({ fileUrl }: ModelViewerProps) => {
const { scene } = useGLTF(fileUrl);
return (
<Canvas style={{ width: "100%", height: "80vh" }}>
<ambientLight />
<directionalLight />
<Stage environment="city" intensity={0.6}>
<primitive object={scene} />
</Stage>
<OrbitControls />
</Canvas>
);
};

View File

@ -1,16 +1,32 @@
import {Box, TextField, Button, Typography, Autocomplete} from '@mui/material'
import {Create} from '@refinedev/mui'
import {useForm} from '@refinedev/react-hook-form'
import {Controller} from 'react-hook-form'
import {
Box,
TextField,
Button,
Typography,
Autocomplete,
} from "@mui/material";
import { Create } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import {MEDIA_TYPES} from '../../lib/constants'
import {ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES, useMediaFileUpload} from '../../components/media/MediaFormUtils'
import { MEDIA_TYPES } from "../../lib/constants";
import {
ALLOWED_IMAGE_TYPES,
ALLOWED_ICON_TYPES,
ALLOWED_PANORAMA_TYPES,
ALLOWED_VIDEO_TYPES,
ALLOWED_WATERMARK_TYPES,
ALLOWED_3D_MODEL_TYPES,
useMediaFileUpload,
} from "../../components/media/MediaFormUtils";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { ModelViewer } from "./ModelViewer/index";
type MediaFormValues = {
media_name: string
media_type: number
file?: File
}
media_name: string;
media_type: number;
file?: File;
};
export const MediaCreate = () => {
const {
@ -24,16 +40,19 @@ export const MediaCreate = () => {
watch,
setError,
clearErrors,
} = useForm<MediaFormValues>({})
getValues,
} = useForm<MediaFormValues>({});
const selectedMediaType = watch('media_type')
const selectedMediaType = watch("media_type");
const file = getValues("file");
const {selectedFile, previewUrl, handleFileChange, handleMediaTypeChange} = useMediaFileUpload({
const { selectedFile, previewUrl, handleFileChange, handleMediaTypeChange } =
useMediaFileUpload({
selectedMediaType,
setError,
clearErrors,
setValue,
})
});
return (
<Create
@ -42,19 +61,20 @@ export const MediaCreate = () => {
...saveButtonProps,
disabled: !!errors.file || !selectedFile,
onClick: handleSubmit((data) => {
console.log(data);
if (data.file) {
const formData = new FormData()
formData.append('media_name', data.media_name)
formData.append('filename', data.file.name)
formData.append('type', String(data.media_type))
formData.append('file', data.file)
const formData = new FormData();
formData.append("media_name", data.media_name);
formData.append("filename", data.file.name);
formData.append("type", String(data.media_type));
formData.append("file", data.file);
console.log('Отправляемые данные:')
console.log("Отправляемые данные:");
for (const pair of formData.entries()) {
console.log(pair[0] + ': ' + pair[1])
console.log(pair[0] + ": " + pair[1]);
}
onFinish(formData)
onFinish(formData);
}
}),
}}
@ -63,30 +83,42 @@ export const MediaCreate = () => {
control={control}
name="media_type"
rules={{
required: 'Это поле является обязательным',
required: "Это поле является обязательным",
}}
render={({ field }) => (
<Autocomplete
options={MEDIA_TYPES}
value={MEDIA_TYPES.find((option) => option.value === field.value) || null}
value={
MEDIA_TYPES.find((option) => option.value === field.value) || null
}
onChange={(_, value) => {
field.onChange(value?.value || null)
handleMediaTypeChange(value?.value || null)
field.onChange(value?.value || null);
handleMediaTypeChange(value?.value || null);
}}
getOptionLabel={(item) => {
return item ? item.label : ''
return item ? item.label : "";
}}
isOptionEqualToValue={(option, value) => {
return option.value === value?.value
return option.value === value?.value;
}}
renderInput={(params) => <TextField {...params} label="Тип" margin="normal" variant="outlined" error={!!errors.media_type} helperText={(errors as any)?.media_type?.message} required />}
renderInput={(params) => (
<TextField
{...params}
label="Тип"
margin="normal"
variant="outlined"
error={!!errors.media_type}
helperText={(errors as any)?.media_type?.message}
required
/>
)}
/>
)}
/>
<TextField
{...register('media_name', {
required: 'Это поле является обязательным',
{...register("media_name", {
required: "Это поле является обязательным",
})}
error={!!(errors as any)?.media_name}
helperText={(errors as any)?.media_name?.message}
@ -98,12 +130,50 @@ export const MediaCreate = () => {
name="media_name"
/>
<Box component="form" sx={{display: 'flex', flexDirection: 'column'}} autoComplete="off" style={{marginTop: 10}}>
<Box display="flex" flexDirection="column-reverse" alignItems="center" gap={6}>
<Box display="flex" flexDirection="column" alignItems="center" gap={2}>
<Button variant="contained" component="label" disabled={!selectedMediaType}>
{selectedFile ? 'Изменить файл' : 'Загрузить файл'}
<input type="file" hidden onChange={handleFileChange} accept={selectedMediaType === 1 ? ALLOWED_IMAGE_TYPES.join(',') : ALLOWED_VIDEO_TYPES.join(',')} />
<Box
component="form"
sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off"
style={{ marginTop: 10 }}
>
<Box
display="flex"
flexDirection="column-reverse"
alignItems="center"
gap={6}
>
<Box
display="flex"
flexDirection="column"
alignItems="center"
gap={2}
>
<Button
variant="contained"
component="label"
disabled={!selectedMediaType}
>
{selectedFile ? "Изменить файл" : "Загрузить файл"}
<input
type="file"
hidden
onChange={handleFileChange}
accept={
selectedMediaType === 6
? ALLOWED_3D_MODEL_TYPES.join(",")
: selectedMediaType === 1
? ALLOWED_IMAGE_TYPES.join(",")
: selectedMediaType === 2
? ALLOWED_VIDEO_TYPES.join(",")
: selectedMediaType === 3
? ALLOWED_ICON_TYPES.join(",")
: selectedMediaType === 4
? ALLOWED_WATERMARK_TYPES.join(",")
: selectedMediaType === 5
? ALLOWED_PANORAMA_TYPES.join(",")
: ""
}
/>
</Button>
{selectedFile && (
@ -121,11 +191,53 @@ export const MediaCreate = () => {
{previewUrl && selectedMediaType === 1 && (
<Box mt={2} display="flex" justifyContent="center">
<img src={previewUrl} alt="Preview" style={{maxWidth: '200px', borderRadius: 8}} />
<img
src={previewUrl}
alt="Preview"
style={{ maxWidth: "200px", borderRadius: 8 }}
/>
</Box>
)}
{file && selectedMediaType === 2 && (
<Box mt={2} display="flex" justifyContent="center">
<video src={URL.createObjectURL(file)} autoPlay controls />
</Box>
)}
{previewUrl && selectedMediaType === 3 && (
<Box mt={2} display="flex" justifyContent="center">
<img
src={previewUrl}
alt="Preview"
style={{ maxWidth: "200px", borderRadius: 8 }}
/>
</Box>
)}
{previewUrl && selectedMediaType === 4 && (
<Box mt={2} display="flex" justifyContent="center">
<img
src={previewUrl}
alt="Preview"
style={{ maxWidth: "200px", borderRadius: 8 }}
/>
</Box>
)}
{file && selectedMediaType === 5 && (
<ReactPhotoSphereViewer
src={URL.createObjectURL(file)}
width={"100%"}
height={"80vh"}
/>
)}
{file && previewUrl && selectedMediaType === 6 && (
<ModelViewer fileUrl={URL.createObjectURL(file)} />
)}
</Box>
</Box>
</Create>
)
}
);
};

View File

@ -11,13 +11,17 @@ import { useEffect } from "react";
import { useShow } from "@refinedev/core";
import { Controller } from "react-hook-form";
import { TOKEN_KEY } from "../../authProvider";
import { MEDIA_TYPES } from "../../lib/constants";
import {
ALLOWED_IMAGE_TYPES,
ALLOWED_VIDEO_TYPES,
ALLOWED_ICON_TYPES,
ALLOWED_WATERMARK_TYPES,
ALLOWED_PANORAMA_TYPES,
ALLOWED_3D_MODEL_TYPES,
useMediaFileUpload,
} from "../../components/media/MediaFormUtils";
import { TOKEN_KEY } from "../../authProvider";
type MediaFormValues = {
media_name: string;
@ -175,7 +179,17 @@ export const MediaEdit = () => {
accept={
selectedMediaType === 1
? ALLOWED_IMAGE_TYPES.join(",")
: ALLOWED_VIDEO_TYPES.join(",")
: selectedMediaType === 2
? ALLOWED_VIDEO_TYPES.join(",")
: selectedMediaType === 3
? ALLOWED_ICON_TYPES.join(",")
: selectedMediaType === 4
? ALLOWED_WATERMARK_TYPES.join(",")
: selectedMediaType === 5
? ALLOWED_PANORAMA_TYPES.join(",")
: selectedMediaType === 6
? ALLOWED_3D_MODEL_TYPES.join(",")
: ""
}
/>
</Button>

View File

@ -1,9 +1,11 @@
import { Stack, Typography, Box, Button } from "@mui/material";
import { useShow } from "@refinedev/core";
import { Show, TextFieldComponent as TextField } from "@refinedev/mui";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import sky from "./12414.jpg";
import { MEDIA_TYPES } from "../../lib/constants";
import { TOKEN_KEY } from "../../authProvider";
import { ModelViewer } from "./ModelViewer/index";
export const MediaShow = () => {
const { query } = useShow({});
@ -43,7 +45,6 @@ export const MediaShow = () => {
)}
{record && record.media_type === 2 && (
<>
<video
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
@ -58,6 +59,63 @@ export const MediaShow = () => {
autoPlay
muted
/>
)}
{record && record.media_type === 3 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
alt={record?.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{record && record.media_type === 4 && (
<img
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
alt={record?.filename}
style={{
maxWidth: "100%",
height: "40vh",
objectFit: "contain",
borderRadius: 8,
}}
/>
)}
{record && record.media_type === 5 && (
<ReactPhotoSphereViewer
src={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
width={"100%px"}
height={"80vh"}
/>
)}
{record && record.media_type === 6 && (
<ModelViewer
fileUrl={`${import.meta.env.VITE_KRBL_MEDIA}${
record?.id
}/download?token=${token}`}
/>
)}
{fields.map(({ label, data, render }) => (
<Stack key={data} gap={1}>
<Typography variant="body1" fontWeight="bold">
{label}
</Typography>
<TextField
value={render ? render(record?.[data]) : record?.[data]}
/>
</Stack>
))}
<Box
sx={{
p: 2,
@ -74,7 +132,7 @@ export const MediaShow = () => {
color: "#FFFFFF",
}}
>
Видео доступно для скачивания по ссылке:
Доступно для скачивания по ссылке:
</Typography>
<Button
variant="contained"
@ -84,22 +142,9 @@ export const MediaShow = () => {
target="_blank"
sx={{ mt: 1, width: "100%" }}
>
Скачать видео
Скачать медиа
</Button>
</Box>
</>
)}
{fields.map(({ label, data, render }) => (
<Stack key={data} gap={1}>
<Typography variant="body1" fontWeight="bold">
{label}
</Typography>
<TextField
value={render ? render(record?.[data]) : record?.[data]}
/>
</Stack>
))}
</Stack>
</Show>
);

View File

@ -10,14 +10,15 @@ import {
import { Edit, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form";
import { Controller } from "react-hook-form";
import { useParams } from "react-router";
import { useParams, Link } from "react-router";
import React, { useState, useEffect } from "react";
import { LinkedItems } from "../../components/LinkedItems";
import { CreateSightArticle } from "../../components/CreateSightArticle";
import { ArticleItem, articleFields } from "./types";
import { TOKEN_KEY } from "../../authProvider";
import { Link } from "react-router";
import Cookies from "js-cookie";
import { observer } from "mobx-react-lite";
import { languageStore } from "../../store/LanguageStore";
function a11yProps(index: number) {
return {
@ -48,21 +49,9 @@ function CustomTabPanel(props: TabPanelProps) {
);
}
export const SightEdit = () => {
export const SightEdit = observer(() => {
const { id: sightId } = useParams<{ id: string }>();
const [language, setLanguage] = useState(Cookies.get("lang") || "ru");
const handleLanguageChange = (lang: string) => {
setLanguage(lang);
};
useEffect(() => {
const lang = Cookies.get("lang")!;
Cookies.set("lang", language);
return () => {
Cookies.set("lang", lang);
};
}, [language]);
const { language, setLanguageAction } = languageStore;
const {
saveButtonProps,
@ -91,9 +80,6 @@ export const SightEdit = () => {
value,
},
],
queryOptions: {
queryKey: ["sight", language],
},
});
const [tabValue, setTabValue] = useState(0);
const { autocompleteProps: mediaAutocompleteProps } = useAutocomplete({
@ -252,10 +238,12 @@ export const SightEdit = () => {
onChange={(_, newValue) => setTabValue(newValue)}
aria-label="basic tabs example"
>
<Tab label="Основная информация" {...a11yProps(0)} />
<Tab label="Статьи" {...a11yProps(1)} />
<Tab label="Основная информация" {...a11yProps(1)} />
<Tab label="Левый виджет" {...a11yProps(2)} />
<Tab label="Правый информация" {...a11yProps(3)} />
</Tabs>
</Box>
<CustomTabPanel value={tabValue} index={0}>
<Edit saveButtonProps={saveButtonProps}>
<Box sx={{ display: "flex", gap: 2 }}>
@ -287,7 +275,7 @@ export const SightEdit = () => {
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("ru")}
onClick={() => setLanguageAction("ru")}
>
RU
</Box>
@ -302,7 +290,7 @@ export const SightEdit = () => {
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("en")}
onClick={() => setLanguageAction("en")}
>
EN
</Box>
@ -317,7 +305,7 @@ export const SightEdit = () => {
p: 1,
borderRadius: 1,
}}
onClick={() => handleLanguageChange("zh")}
onClick={() => setLanguageAction("zh")}
>
ZH
</Box>
@ -896,14 +884,10 @@ export const SightEdit = () => {
</Box>
</Paper>
</Box>
</Edit>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}>
{sightId && (
<Box sx={{ mt: 3 }}>
<LinkedItems<ArticleItem>
type="edit"
parentId={sightId}
parentId={sightId!}
parentResource="sight"
childResource="article"
fields={articleFields}
@ -911,14 +895,20 @@ export const SightEdit = () => {
/>
<CreateSightArticle
parentId={sightId}
parentId={sightId!}
parentResource="sight"
childResource="article"
title="статью"
/>
</Box>
)}
</Edit>
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={1}>
1
</CustomTabPanel>
<CustomTabPanel value={tabValue} index={2}>
2
</CustomTabPanel>
</Box>
);
};
});

View File

@ -22,7 +22,7 @@ export const SightList = observer(() => {
{
field: "cityID",
operator: "eq",
value: city_id,
value: city_id === "0" ? null : city_id,
},
],
},

View File

@ -23,7 +23,7 @@ export const StationList = observer(() => {
{
field: "cityID",
operator: "eq",
value: city_id,
value: city_id === "0" ? null : city_id,
},
],
},

View File

@ -1,39 +0,0 @@
.attraction-card {
height: 415px;
width: 315px;
background: linear-gradient(
113.51deg,
rgba(255, 255, 255, 0) 8.71%,
rgba(255, 255, 255, 0.16) 69.69%
),
#806c59;
border-radius: 10px;
overflow: hidden;
color: #ffffff;
}
.attraction-card__content {
padding: 12px;
}
.attraction-card__title {
margin: 8px 0 0 0;
}
.attraction-card__text {
margin: 30px 0 0 0;
white-space: pre-wrap;
}
.attraction-card__subtitle {
margin: 8px 0 0 0;
font-weight: 400;
}
.attraction-card__image {
min-width: 100%;
max-height: 50%;
padding: 2px;
border-radius: 10px 10px 0 0;
}

View File

@ -1,55 +0,0 @@
import "./AttractionShortPreview.css";
import { LocalizedString, useServerLocalization } from "@mt/i18n";
import classNames from "classnames";
import { TouchScrollWrapper } from "../TouchScrollWrapper/TouchScrollWrapper";
import { HTMLAttributes } from "react";
export interface AttractionShortPreviewProps
extends Omit<HTMLAttributes<HTMLElement>, "title" | "content"> {
img: string;
title: LocalizedString;
subtitle: LocalizedString;
content: LocalizedString;
}
export function AttractionShortPreview({
img,
title,
subtitle,
content,
className,
...props
}: AttractionShortPreviewProps) {
const localizeText = useServerLocalization();
return (
<div
className={classNames(className, "attraction-card g-flex-column")}
{...props}
>
{img && (
<img
className="attraction-card__image"
src={img}
alt={localizeText(title)}
/>
)}
<TouchScrollWrapper className="g-flex-column__item">
<div className="attraction-card__content">
<h4 className="attraction-card__title">{localizeText(title)}</h4>
<h5 className="attraction-card__subtitle">
{localizeText(subtitle)}
</h5>
<p
className="attraction-card__text"
dangerouslySetInnerHTML={{ __html: localizeText(content) }}
/>
</div>
</TouchScrollWrapper>
</div>
);
}

View File

@ -1,126 +0,0 @@
.widget-container {
width: 545px;
height: var(--attraction-widget-container-height, 100%);
max-height: calc(100% - 90px);
color: #ffffff;
background: #806c59;
border: 2px solid #806c59;
border-radius: 10px;
}
.widget-content {
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.widget-slide {
position: relative;
display: none;
top: 0;
left: 0;
flex-direction: column;
justify-content: flex-start;
align-items: center;
width: 100%;
}
.widget-slide.active,
.widget-slide.preview {
display: flex;
flex: 1;
overflow: auto;
}
.widget-media {
width: 100%;
height: auto;
max-height: 644px;
}
.view-container {
border-radius: 8px 8px 0 0;
}
.widget-header {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
rgba(179, 165, 152, 0.4);
box-shadow: inset 4px 4px 12px rgba(255, 255, 255, 0.12);
width: 100%;
padding: 9px 16px;
font-weight: 700;
font-size: 24px;
line-height: 120%;
}
.widget-text {
width: 100%;
align-self: self-start;
padding: 16px;
font-weight: 400;
font-size: 18px;
line-height: 150%; /* or 27px */
opacity: 0;
transition: opacity 0.5s ease-in-out;
user-select: none;
word-break: break-word;
white-space: pre-wrap;
}
.widget-text p {
margin: 0;
}
.widget-text.preview {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
font-weight: 700;
font-size: 48px;
line-height: 120%;
text-align: center;
}
.widget-text.active {
opacity: 1;
}
.widget-titles {
display: flex;
height: 50px;
justify-content: space-evenly;
align-items: center;
width: 100%;
margin: 5px 0 0 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
rgba(179, 165, 152, 0.4);
box-shadow: inset 4px 4px 12px rgba(255, 255, 255, 0.12);
border-radius: 0 0 10px 10px;
padding: 12px 0;
}
.widget-title {
font-weight: 400;
font-size: 18px;
line-height: 21px;
cursor: pointer;
user-select: none;
width: 100px;
text-align: center;
}
.widget-title.active {
font-weight: bold;
text-decoration: underline;
text-underline-offset: 5px;
}
.widget-title.preview {
display: none;
}

View File

@ -1,114 +0,0 @@
import React, { HTMLAttributes, useEffect, useState } from "react";
import { useServerLocalization } from "@mt/i18n";
import cn from "classnames";
import { useSwipeable } from "react-swipeable";
import { ArticleBase } from "@mt/common-types";
import "./AttractionWidget.css";
import { usePrevious } from "@mt/utils";
import { AttractionMedia } from "./media/AttractionMedia";
import { TouchScrollWrapper } from "../TouchScrollWrapper/TouchScrollWrapper";
export interface AttractionsWidgetProps extends HTMLAttributes<HTMLElement> {
articles: ArticleBase[];
isIdleMode: boolean;
isPreviewOnly?: boolean;
}
export function AttractionWidget({
articles,
isIdleMode,
isPreviewOnly = false,
...props
}: AttractionsWidgetProps) {
const [activeIndex, setActiveIndex] = useState(0);
const prevArticles = usePrevious<ArticleBase[]>(articles) || [];
const localizeText = useServerLocalization();
const swipeHandlers = useSwipeable({
onSwipedLeft: ({ event }) => {
event.preventDefault();
setActiveIndex((activeIndex) => (activeIndex + 1) % articles.length);
},
onSwipedRight: ({ event }) => {
event.preventDefault();
setActiveIndex(
(activeIndex) => (activeIndex - 1 + articles.length) % articles.length
);
},
swipeDuration: 500,
preventScrollOnSwipe: true,
trackMouse: true,
});
const handleClick = (index: number) => {
setActiveIndex(index);
document.querySelector(".widget-text.active")!.scrollTop = 0;
};
useEffect(() => setActiveIndex(activeIndex), [activeIndex]);
useEffect(() => {
if (
!isPreviewOnly &&
(isIdleMode || JSON.stringify(prevArticles) !== JSON.stringify(articles))
) {
setActiveIndex(0);
}
// admin specific case: during edit we removed active article
if (prevArticles?.length > articles?.length) {
setActiveIndex(0);
}
}, [isPreviewOnly, isIdleMode, articles]);
return (
<div className="widget-container g-flex-column__item-fixed" {...props}>
<div className="widget-content">
{articles?.map((article, index) => (
<div
key={index}
className={`widget-slide ${index === activeIndex ? "active" : ""}`}
onPointerUp={() => handleClick(index)}
>
<div className="widget-media">
<AttractionMedia media={article.media} />
</div>
{index !== 0 && (
<div className="widget-header">
{localizeText(articles[0].text)}
</div>
)}
<TouchScrollWrapper
className={cn("widget-text", {
active: index === activeIndex,
preview: article.isPreview,
})}
>
<div
dangerouslySetInnerHTML={{ __html: localizeText(article.text) }}
{...swipeHandlers}
/>
</TouchScrollWrapper>
</div>
))}
<div className="widget-titles">
{articles?.map((article, index) => (
<div
key={`title-${index}`}
className={cn("widget-title", {
active: index === activeIndex,
preview: article.isPreview,
})}
onPointerUp={() => handleClick(index)}
>
{localizeText(article.name)}
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -1,52 +0,0 @@
.widget-image,
.widget-video,
.widget-3d-model {
max-width: 100%;
max-height: 100%;
width: 100%;
height: 100%;
display: block;
border-radius: 8px 8px 0 0;
}
.widget-3d-model {
height: 350px;
}
.widget-media__wrapper {
position: relative;
/*TODO: it worth to investigate it further... quite weird behavior of */
box-sizing: content-box !important;
}
.fullscreen-photo-sphere-btn,
.fullscreen-3d-btn {
width: 20px;
height: 20px;
position: absolute;
right: 10px;
bottom: 10px;
cursor: pointer;
z-index: 100;
opacity: 0.7;
}
.media-with-watermark {
position: relative;
}
.watermark {
position: absolute;
top: 10px;
left: 10px;
width: 50px;
height: auto;
}
.psv-autorotate-button {
display: block !important;
}
.psv-menu-button {
display: none !important;
}

View File

@ -1,36 +0,0 @@
import { Media } from "@mt/common-types";
import { ImageMedia } from "./ImageMedia";
import { VideoMedia } from "./VideoMedia";
import { PhotoSphereMedia } from "./PhotoSphereMedia";
import { Object3DMedia } from "./Object3DMedia";
import { memo } from "react";
export const AttractionMedia = memo(
({ media }: { media: Media }) => {
const { type, url, watermarkUrl } = media;
if (!url) return null;
switch (type) {
case "IMAGE":
return (
<ImageMedia url={url} alt={media.url} watermarkUrl={watermarkUrl} />
);
case "VIDEO":
return <VideoMedia url={url} watermarkUrl={watermarkUrl} />;
case "PHOTO_SPHERE":
return <PhotoSphereMedia url={url} watermarkUrl={watermarkUrl} />;
case "OBJECT_3D":
return <Object3DMedia url={url} watermarkUrl={watermarkUrl} />;
default:
return null;
}
},
({ media }, { media: newMedia }) => {
return (
media.url === newMedia.url &&
media.watermarkUrl === newMedia.watermarkUrl &&
media.type === newMedia.type
);
}
);

View File

@ -1,25 +0,0 @@
import cn from 'classnames';
import React from 'react';
import './AttractionMedia.css';
interface ImageMediaProps {
url: string;
alt: string;
watermarkUrl?: string;
}
export const ImageMedia = ({ url, alt, watermarkUrl }: ImageMediaProps) => (
<>
<img
src={url}
alt={alt}
className={cn('widget-image', {
'media-with-watermark': watermarkUrl !== null,
})}
/>
{watermarkUrl && (
<img src={watermarkUrl} alt="Watermark" className="watermark" />
)}
</>
);

View File

@ -1,52 +0,0 @@
import cn from "classnames";
import React, { useEffect, useState } from "react";
import "./AttractionMedia.css";
import ModelViewer from "../../model-viewer/ModelViewer";
import { Icons, useLightboxContext } from "@mt/components";
import { Object3DLightboxData } from "@mt/common-types";
interface Object3DMediaProps {
url: string;
watermarkUrl?: string;
}
export const Object3DMedia = ({ url, watermarkUrl }: Object3DMediaProps) => {
// prettier-ignore
const { setData, openLightbox } = useLightboxContext<Object3DLightboxData>();
const [autoRotate, setAutoRotate] = useState(true);
const handle3DFullscreenOpen = () => {
setAutoRotate(false);
setData({
type: "OBJECT_3D",
modelUrl: url,
watermarkUrl,
});
openLightbox();
};
useEffect(() => {
setAutoRotate(true);
}, [url]);
return (
<div className="widget-media__wrapper">
<div
className={cn("widget-3d-model", {
"media-with-watermark": watermarkUrl !== null,
})}
>
<ModelViewer key={url} pathToModel={url} autoRotate={autoRotate} />
{watermarkUrl && (
<img src={watermarkUrl} alt="Watermark" className="watermark" />
)}
</div>
<Icons.FullscreenIcon
className="fullscreen-3d-btn"
onPointerUp={() => handle3DFullscreenOpen()}
/>
</div>
);
};

View File

@ -1,62 +0,0 @@
import cn from "classnames";
import React, { useRef } from "react";
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer";
import { PhotoSphereLightboxData } from "@mt/common-types";
import "./AttractionMedia.css";
import { useLightboxContext } from "../../lightbox";
import { Icons } from "@mt/components";
interface PhotoSphereMediaProps {
url: string;
watermarkUrl?: string;
}
export const PhotoSphereMedia = ({
url,
watermarkUrl,
}: PhotoSphereMediaProps) => {
// prettier-ignore
const { setData, openLightbox } = useLightboxContext<PhotoSphereLightboxData>();
// react-photo-sphere-viewer doesn't have exported types, so here's a bit of a hardcoded piece
const photoSphereRef = useRef<any>(null);
const handlePhotoSphereFullscreenOpen = () => {
photoSphereRef.current?.stopAutoRotate();
setData({
type: "PHOTO_SPHERE",
imageUrl: url,
watermarkUrl,
});
openLightbox();
};
return (
<div className="widget-media__wrapper">
<ReactPhotoSphereViewer
ref={photoSphereRef}
key={url}
src={url}
height={"350px"}
width={"100%"}
container={cn("widget-media", {
"media-with-watermark": watermarkUrl !== null,
})}
moveInertia={false}
mousemove={true}
navbar={["autorotate", "zoom"]}
keyboard={false}
loadingTxt="Загрузка..."
/>
{watermarkUrl && (
<img src={watermarkUrl} alt="Watermark" className="watermark" />
)}
{/* the following is a workaround to open lightbox-like preview in the middle of the screen instead of the real fullscreen */}
<Icons.FullscreenIcon
className="fullscreen-photo-sphere-btn"
onPointerUp={() => handlePhotoSphereFullscreenOpen()}
/>
</div>
);
};

View File

@ -1,26 +0,0 @@
import cn from "classnames";
import React from "react";
import "./AttractionMedia.css";
interface VideoMediaProps {
url: string;
watermarkUrl?: string;
}
export const VideoMedia = ({ url, watermarkUrl }: VideoMediaProps) => (
<>
<video
src={url}
className={cn("widget-video", {
"media-with-watermark": watermarkUrl !== null,
})}
autoPlay
loop
muted
/>
{watermarkUrl && (
<img src={watermarkUrl} alt="Watermark" className="watermark" />
)}
</>
);

View File

@ -1,47 +0,0 @@
// TODO: rewrite as css module
import styled from '@emotion/styled';
export const StyledDrawer = styled.div`
z-index: 1000;
position: absolute;
width: 290px;
height: 100%;
transition: all ease-in-out 0.3s;
transform: translateX(-100%);
color: #fff;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
#806c59;
&.nav-widget--opened {
transform: translateX(0);
.toggle-btn {
transform: rotate(0);
}
}
.actions {
position: absolute;
bottom: 12px;
left: 310px;
}
.action-btn {
width: 48px;
height: 48px;
display: block;
cursor: pointer;
margin-right: 16px;
}
.toggle-btn {
transform: rotate(180deg);
}
.order-btn-inverse {
transform: scale(1, -1);
}
`;

View File

@ -1,53 +0,0 @@
import cn from "classnames";
import { HTMLAttributes, ReactNode } from "react";
import { StyledDrawer } from "./Drawer.styles";
import { Icons } from "@mt/components";
import { Locale, LocaleSwitcher } from "@mt/i18n";
export interface DrawerProps extends HTMLAttributes<HTMLDivElement> {
onToggle: (isOpened: boolean) => void;
isOpen: boolean;
onHomeBtnClick?: () => void;
onLocaleChange: (locale: Locale) => void;
actions?: ReactNode;
}
// TODO: consider refactoring - drawer and controls should be separated
export function Drawer({
children,
isOpen,
onToggle,
onHomeBtnClick,
onLocaleChange,
actions,
...props
}: DrawerProps) {
return (
<StyledDrawer
className={cn("g-flex-column", { "nav-widget--opened": isOpen })}
{...props}
>
{children}
<div className="g-flex actions">
<div
className="action-btn toggle-btn"
onPointerUp={() => onToggle(!isOpen)}
>
<Icons.ArrowBtn />
</div>
{isOpen && (
<div className="action-btn" onPointerUp={() => onHomeBtnClick?.()}>
<Icons.HomeBtn />
</div>
)}
{actions}
<LocaleSwitcher onLocaleChange={onLocaleChange} />
</div>
</StyledDrawer>
);
}

View File

@ -1 +0,0 @@
export { Drawer } from './Drawer';

View File

@ -1,274 +0,0 @@
import React, {
createContext,
useState,
useContext,
ReactNode,
useMemo,
useEffect,
} from "react";
import { geoMercator } from "d3-geo";
import { Coordinates, Track, uuid } from "@mt/common-types";
import { useNearStation, usePassedTrackIndex } from "./hooks";
import { AttractionGroup, MapData, StationOnMap } from "./map-widget.interface";
import { getMapPoint } from "./utils";
import { EMPTY_SETTING_VALUE, zeroCoordinates } from "./map-widget.constant";
import {
MapSettings,
MapWidgetContextType,
} from "./map-widget-context.interface";
import { useForm } from "react-hook-form";
export const mapCanvasProps = {
style: {
width: "100%",
height: "100%",
},
width: 500,
height: 400,
};
// prettier-ignore
export const MapWidgetContext = createContext<MapWidgetContextType | undefined>(undefined);
export const MapWidgetProvider = ({ children }: { children: ReactNode }) => {
const [track, setTrack] = useState<Track | null>(null);
const [stations, setStations] = useState<StationOnMap[]>([]);
const [updatedStationIds, setUpdatedStationIds] = useState<uuid[]>([]);
const [attractionGroups, setAttractionGroups] = useState<AttractionGroup[]>(
[]
);
const [rotateAngle, setRotateAngle] = useState<number>(0);
const [scale, setScale] = useState<number>(0);
const [fullScale, setFullScale] = useState<number>(0);
const [zoomedScale, setZoomedScale] = useState<number>(0);
const [center, setCenter] = useState(zeroCoordinates);
const [baseCenter, setBaseCenter] = useState(zeroCoordinates);
const [currentPosition, setCurrentPosition] = useState<Coordinates | null>(
null
);
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const [isDragMode, setIsDragMode] = useState<boolean>(false);
const [initialSettingsData, setInitialSettingsData] =
useState<MapSettings>(EMPTY_SETTING_VALUE);
const [isSettingsDataChanged, setIsSettingsDataChanged] =
useState<boolean>(false);
const isMapDataChanged = useMemo(
() => isSettingsDataChanged || updatedStationIds.length > 0,
[isSettingsDataChanged, updatedStationIds]
);
const stationsMap = useMemo(
() => new Map(stations.map((station) => [station.id, station])),
[stations]
);
const middleTrackCoordinates: Coordinates | null = useMemo(() => {
if (!track?.length) {
return null;
}
const middleTrackIndex = Math.floor(track.length / 2);
return track[middleTrackIndex];
}, [track]);
const settingsForm = useForm({
mode: "onChange",
reValidateMode: "onChange",
defaultValues: initialSettingsData,
});
useEffect(
() => settingsForm.reset(initialSettingsData),
[initialSettingsData]
);
const onMapDataFetched = (data: MapData) => {
setTrack(data.trackPoints);
setStations(data.stationsOnMap);
setAttractionGroups(data.touristAttractionGroupsOnMap);
setRotateAngle(data.mapRotateAngle);
setCenter(data.centerOfMapPoint);
setBaseCenter(data.centerOfMapPoint);
setScale(data.fullMapScale);
setFullScale(data.fullMapScale);
setZoomedScale(data.zoomedMapScale);
setInitialSettingsData({
rotateAngle: data.mapRotateAngle,
center: data.centerOfMapPoint,
fullScale: data.fullMapScale,
zoomedScale: data.zoomedMapScale,
});
setIsSettingsDataChanged(false);
setUpdatedStationIds([]);
};
const onSettingsFormChange = () => {
const formData = settingsForm.getValues();
const { center, rotateAngle, fullScale, zoomedScale, currentStationId } =
formData;
setBaseCenter(center);
setRotateAngle(rotateAngle);
setFullScale(fullScale);
setZoomedScale(zoomedScale);
if (currentStationId) {
const { pointOnMap } = stationsMap.get(currentStationId) as StationOnMap;
setCenter(pointOnMap);
setScale(zoomedScale);
setCurrentPosition(pointOnMap);
setIsDragMode(false);
} else {
setCenter(center);
setScale(fullScale);
setCurrentPosition(middleTrackCoordinates);
}
updateMapDataChanged(formData);
};
const onMapCenterMoved = (center: Coordinates) => {
setBaseCenter(center);
setCenter(center);
settingsForm.setValue("center", center, { shouldDirty: true });
updateMapDataChanged(settingsForm.getValues());
};
const updateMapDataChanged = (data: MapSettings) => {
const { rotateAngle, center, fullScale, zoomedScale } = data;
setIsSettingsDataChanged(
JSON.stringify({
rotateAngle,
center,
fullScale,
zoomedScale,
}) !== JSON.stringify(initialSettingsData)
);
};
const onStationUpdate: MapWidgetContextType["onStationUpdate"] = (
stationId,
{ labelOffset, labelAlignment }
) => {
const updatedStation = {
...(stationsMap.get(stationId) as StationOnMap),
...(labelOffset && { labelOffset }),
...(labelAlignment && { labelAlignment }),
};
setStations((stations) =>
stations.map((station) =>
station.id === stationId ? updatedStation : station
)
);
setUpdatedStationIds((ids) => [...ids, stationId]);
};
const getUpdatedStations = () => {
return updatedStationIds.reduce((acc: Record<uuid, any>, id: uuid) => {
const { labelAlignment, labelOffset } = stationsMap.get(
id
) as StationOnMap;
acc[id] = {
textAlignment: labelAlignment,
mapOffsets: labelOffset,
};
return acc;
}, {});
};
const projection = useMemo(() => {
const { width, height } = mapCanvasProps;
return geoMercator()
.translate([width / 2, height / 2])
.center(getMapPoint(center))
.scale(scale);
}, [center, scale]);
const { passedTrackIndex } = usePassedTrackIndex(track, currentPosition);
const { currentStation, nextStation, isOnStation } = useNearStation(
currentPosition,
stations,
passedTrackIndex
);
// Bind map center and zoom to currentStation in not EditMode
useEffect(() => {
if (isEditMode) {
return;
}
if (currentStation) {
const { pointOnMap } = currentStation;
setCenter(pointOnMap);
setScale(zoomedScale);
} else {
setCenter(baseCenter);
setScale(fullScale);
}
}, [currentStation]);
const contextValue = {
track,
center,
rotateAngle,
projection,
attractionGroups,
stations,
currentPosition,
middleTrackCoordinates,
passedTrackIndex,
currentStation,
isOnStation,
nextStation,
setCurrentPosition,
isDragMode,
setIsDragMode,
isEditMode,
setIsEditMode,
onMapDataFetched,
settingsForm,
onSettingsFormChange,
onMapCenterMoved,
onStationUpdate,
getUpdatedStations,
isMapDataChanged,
};
return (
<MapWidgetContext.Provider value={contextValue}>
{children}
</MapWidgetContext.Provider>
);
};
export const useMapWidgetContext = function (): MapWidgetContextType {
const context = useContext(MapWidgetContext);
if (!context) {
throw new Error(
"useMapWidgetContext must be used within a MapWidgetProvider"
);
}
return context;
};

View File

@ -1,25 +0,0 @@
import { TrackAttractions, TrackLine, TrackStations, TramMarker } from '../index';
import { getMapPoint } from '../../utils';
import { useMapWidgetContext } from '../../MapWidgetContext';
export const MapContent = () => {
const { rotateAngle, isEditMode, currentPosition, currentStation, nextStation } =
useMapWidgetContext();
return (
<g className="g-transform-origin__center" style={{ transform: `rotate(${rotateAngle}deg)` }}>
<TrackLine />
<TrackAttractions />
<TrackStations />
{!isEditMode && currentPosition && nextStation && (
<TramMarker
coordinates={getMapPoint(currentPosition)}
nextStopPoint={(currentStation ?? nextStation).pointOnMap}
/>
)}
</g>
);
};

View File

@ -1,4 +0,0 @@
.mapWidget {
position: relative;
z-index: 100;
}

View File

@ -1,65 +0,0 @@
import {
ComposableMap,
ZoomableGroup,
ZoomableGroupProps,
} from "react-simple-maps";
import styles from "./MapWidget.module.css";
import { mapCanvasProps, useMapWidgetContext } from "../../MapWidgetContext";
import { useState, FC, ReactNode } from "react";
import { MapContent } from "./MapContent";
// Create wrapper components to handle type issues
const ComposableMapWrapper: FC<any> = (props) => {
// @ts-ignore - Ignore type issues with the ComposableMap component
return <ComposableMap {...props} />;
};
const ZoomableGroupWrapper: FC<ZoomableGroupProps> = (props) => {
// @ts-ignore - Ignore type issues with the ZoomableGroup component
return <ZoomableGroup {...props} />;
};
// default coordinates for 3a route: 59.943, 30.331
export const MapWidget = () => {
const { onMapCenterMoved, projection, isDragMode, rotateAngle } =
useMapWidgetContext();
const [key, setKey] = useState(42);
const handleMoveEnd: ZoomableGroupProps["onMoveEnd"] = (e, d3Zoom) => {
const { PI, cos, sin } = Math;
const { x, y } = d3Zoom.transform;
const { width, height } = mapCanvasProps;
const alpha = (-rotateAngle * PI) / 180;
const x1 = x * cos(alpha) - y * sin(alpha);
const y1 = x * sin(alpha) + y * cos(alpha);
const cX = width / 2 - x1;
const cY = height / 2 - y1;
const [lon, lat] = projection.invert?.([cX, cY]) ?? [0, 0];
onMapCenterMoved({ lon, lat });
setKey(-key);
};
return (
<ComposableMapWrapper
projection={projection as any}
className={styles.mapWidget}
{...mapCanvasProps}
>
<ZoomableGroupWrapper
key={key}
center={projection.center()}
onMoveEnd={handleMoveEnd}
filterZoomEvent={() => isDragMode}
minZoom={1}
maxZoom={1}
>
<MapContent />
</ZoomableGroupWrapper>
</ComposableMapWrapper>
);
};

View File

@ -1 +0,0 @@
export * from './MapWidget';

View File

@ -1,41 +0,0 @@
.markerLarge,
.markerSmall {
overflow: visible;
}
.markerLarge {
width: 23px;
}
.markerLarge .counter {
transform: translate(30%, 15%);
}
.markerSmall {
width: 15px;
}
.markerSmall .counter {
transform: translate(50%, -25%);
}
.icon {
width: 100%;
}
.counter {
position: absolute;
top: 0;
right: 0;
width: 8px;
height: 8px;
line-height: 8px;
text-align: center;
border-radius: 50%;
background-color: #896f58;
font-size: 0.3rem;
font-weight: 700;
color: #ffffff;
}

View File

@ -1,40 +0,0 @@
import { Point, Marker } from "react-simple-maps";
import { Icons } from "@mt/components";
import { AttractionGroupIconSizeType } from "@mt/common-types";
import styles from "./AttractionMarker.module.css";
import cn from "classnames";
interface Props {
coordinates: Point;
rotate: number;
size: AttractionGroupIconSizeType;
counter?: number;
}
export const AttractionMarker = ({
coordinates,
counter = 0,
rotate,
size,
}: Props) => {
return (
<Marker coordinates={coordinates}>
<foreignObject
className={cn({
[styles.markerLarge]: size === "LARGE",
[styles.markerSmall]: size === "SMALL",
})}
>
<div
className="g-transform-origin__center"
style={{ transform: `rotate(${rotate}deg)` }}
>
<Icons.AttractionIcon className={styles.icon} />
{counter > 1 && <div className={styles.counter}>{counter}</div>}
</div>
</foreignObject>
</Marker>
);
};

View File

@ -1,25 +0,0 @@
import { AttractionMarker } from "./AttractionMarker";
import { getMapPoint } from "../../utils";
import { useMapWidgetContext } from "../../MapWidgetContext";
export const TrackAttractions = () => {
const { attractionGroups, rotateAngle } = useMapWidgetContext();
return (
<>
{attractionGroups.map((group) => (
<AttractionMarker
key={
group.touristAttractionsOnMap[0]?.id ||
`${group.pointOnMap.lat}:${group.pointOnMap.lon}`
}
coordinates={getMapPoint(group.pointOnMap)}
// Inverse angle to compensate map rotation
rotate={-rotateAngle}
counter={group.touristAttractionsOnMap.length}
size={group.iconSize}
/>
))}
</>
);
};

View File

@ -1 +0,0 @@
export * from './TrackAttractions';

View File

@ -1,37 +0,0 @@
import { useMemo } from "react";
import { Line, Point } from "react-simple-maps";
import { getMapPoint } from "../utils";
import { useMapWidgetContext } from "../MapWidgetContext";
import { zeroCoordinates } from "../map-widget.constant";
const passedTrackColor = "#ed1c24";
const trackColor = "#cccccc";
export const TrackLine = () => {
const { track, passedTrackIndex, currentPosition } = useMapWidgetContext();
const mappedTrack: Point[] = useMemo(
() => (track ? track.map(({ lat, lon }) => [lon, lat]) : []),
[track]
);
return (
<>
<Line
coordinates={mappedTrack}
strokeWidth={2.5}
strokeLinecap="round"
stroke={trackColor}
/>
<Line
coordinates={[
...mappedTrack.slice(0, passedTrackIndex),
getMapPoint(currentPosition ?? zeroCoordinates),
]}
strokeWidth={3.5}
strokeLinecap="round"
stroke={passedTrackColor}
/>
</>
);
};

View File

@ -1,8 +0,0 @@
// TODO: resolve circular deps
import { StationLabelContent, StationLabelContentProps } from './StationLabelContent';
export const StationLabel = ({ station }: StationLabelContentProps) => (
<foreignObject className="track-station" {...station.labelOffset}>
<StationLabelContent station={station} />
</foreignObject>
);

View File

@ -1,55 +0,0 @@
import { HTMLAttributes, ReactNode, useContext } from 'react';
import { StationOnMap, TransportIcon, useMapWidgetContext } from '@mt/components';
import { OnMapTextAlignment } from '@mt/common-types';
import { LocalizationContext } from '@mt/i18n';
export interface StationLabelContentProps extends HTMLAttributes<HTMLDivElement> {
station: StationOnMap;
children?: ReactNode;
}
type TextAlign = Lowercase<OnMapTextAlignment>;
export const StationLabelContent = ({
station,
children,
className = '',
}: StationLabelContentProps) => {
const { locale } = useContext(LocalizationContext);
const { rotateAngle } = useMapWidgetContext();
const { pointOnMap, labelAlignment, iconUrl, shortName, name, transferStationInfos } = station;
return (
<div
id={`${pointOnMap.lat}:${pointOnMap.lon}`}
className={`track-station__wrapper ${className}`}
style={{
textAlign: labelAlignment as TextAlign,
// Inverse angle to compensate map rotation
transform: `rotate(${-rotateAngle}deg)`,
}}
>
<div className="track-station__label">
{iconUrl && <img className="track-station__icon" src={iconUrl} />}
{(shortName ?? name).ru}
</div>
<div className="track-station__transfers-wrapper">
{transferStationInfos.map((transfer) => (
<div className="track-station__label" key={transfer.name.ru}>
<TransportIcon type={transfer.type} className="transport-icon" />
{transfer.name.ru}
</div>
))}
</div>
<div className="track-station__label-locale">
{locale === 'zh' ? (shortName ?? name).zh : (shortName ?? name).en}
</div>
{children}
</div>
);
};

View File

@ -1,63 +0,0 @@
import { useState } from 'react';
import Draggable, { DraggableData, DraggableEvent } from 'react-draggable';
import { ButtonGroup, IconButton } from '@mui/material';
import AlignHorizontalLeftRoundedIcon from '@mui/icons-material/AlignHorizontalLeftRounded';
import AlignHorizontalCenterRoundedIcon from '@mui/icons-material/AlignHorizontalCenterRounded';
import AlignHorizontalRightRoundedIcon from '@mui/icons-material/AlignHorizontalRightRounded';
// TODO: resolve circular deps
import { OnMapOffset, OnMapTextAlignment } from '@mt/common-types';
import { useMapWidgetContext } from '@mt/components';
import { StationLabelContent, StationLabelContentProps } from './StationLabelContent';
const CONTAINER_WIDTH = 1343;
const SVG_WIDTH = 500;
export const StationLabelEdit = ({ station }: StationLabelContentProps) => {
const { onStationUpdate } = useMapWidgetContext();
const { id, labelOffset } = station;
const [calculatedOffset] = useState<OnMapOffset>(labelOffset);
const [dragStartPoint, setDragStartPoint] = useState<OnMapOffset>({ x: -1, y: -1 });
const onDragStart = () => setDragStartPoint(calculatedOffset);
const onDragStop = (e: DraggableEvent, { lastX, lastY }: DraggableData) => {
const labelOffset = {
x: dragStartPoint.x + lastX,
y: dragStartPoint.y + lastY,
};
onStationUpdate(id, { labelOffset });
};
const setAlignment = (labelAlignment: OnMapTextAlignment): void =>
onStationUpdate(id, { labelAlignment });
return (
<Draggable
onStart={onDragStart}
onStop={onDragStop}
scale={CONTAINER_WIDTH / SVG_WIDTH}
positionOffset={calculatedOffset}
>
<foreignObject className="track-station">
<StationLabelContent station={station} className="editable">
<ButtonGroup size="small" className="align-btns-group">
<IconButton size="small" onClick={() => setAlignment('LEFT')}>
<AlignHorizontalLeftRoundedIcon />
</IconButton>
<IconButton size="small" onClick={() => setAlignment('CENTER')}>
<AlignHorizontalCenterRoundedIcon />
</IconButton>
<IconButton size="small" onClick={() => setAlignment('RIGHT')}>
<AlignHorizontalRightRoundedIcon />
</IconButton>
</ButtonGroup>
</StationLabelContent>
</foreignObject>
</Draggable>
);
};

View File

@ -1,68 +0,0 @@
/* foreignObject */
.track-station {
overflow: visible;
}
.track-station__wrapper {
transform-origin: left top;
transform-box: fill-box;
position: absolute;
top: 0;
left: 0;
}
.track-station__wrapper.editable {
cursor: move;
}
.track-station__label,
.track-station__label-locale {
white-space: nowrap;
}
.track-station__label {
color: #ffffff;
font-size: 0.32rem;
line-height: 1;
letter-spacing: initial;
}
.track-station__label-locale {
color: #cbcbcb;
font-size: 0.25rem;
}
.track-station__transfers-wrapper:not(:empty) {
margin: 1.5px 0;
}
.align-btns-group {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
background-color: #ffffff;
display: none !important;
}
.track-station__wrapper:hover .align-btns-group {
display: flex !important;
}
.align-btns-group > button {
width: 10px;
height: 10px;
}
.transport-icon,
.track-station__icon {
display: inline-block;
margin-right: 1px;
vertical-align: middle;
}
.transport-icon,
.track-station__icon,
.track-station svg {
width: 6px;
height: 6px;
}

View File

@ -1,71 +0,0 @@
import { Marker } from "react-simple-maps";
// TODO: resolve circular deps
import type { uuid } from "@mt/common-types";
import { getMapPoint } from "../../utils";
import "./TrackStations.css";
import { StationLabelEdit } from "./StationLabelEdit";
import { useMapWidgetContext } from "../../MapWidgetContext";
import { StationLabel } from "./StationLabel";
const colors = {
black: "#000000",
red: "#ed1c24",
grey: "#cccccc",
yellow: "#fcd500",
};
export const TrackStations = () => {
const {
stations,
currentStation,
passedTrackIndex,
isOnStation,
isEditMode,
} = useMapWidgetContext();
const isTerminalStation = (index: number) =>
index === 0 || index === stations.length - 1;
const getStationFill = (
id: uuid,
trackIndex: number,
index: number
): string => {
if (isOnStation && currentStation?.id === id) {
return colors.yellow;
}
if (isTerminalStation(index)) {
return colors.black;
}
return trackIndex <= passedTrackIndex ? colors.red : colors.grey;
};
const getStationStroke = (index: number) => {
if (index === 0) return colors.red;
if (index === stations.length - 1) return colors.grey;
return colors.black;
};
return (
<>
{stations.map((it, index) => (
<Marker key={it.id} coordinates={getMapPoint(it.pointOnMap)}>
<circle
fill={getStationFill(it.id, it.pointOnMap.trackIndex, index)}
stroke={getStationStroke(index)}
r={3.5}
strokeWidth={isTerminalStation(index) ? 2 : 1.5}
/>
{isEditMode ? (
<StationLabelEdit station={it} />
) : (
<StationLabel station={it} />
)}
</Marker>
))}
</>
);
};

View File

@ -1 +0,0 @@
export * from './TrackStations';

View File

@ -1,36 +0,0 @@
.icon {
width: 100%;
height: 100%;
}
.iconContainer {
position: relative;
overflow: visible;
border-radius: 50%;
background: #e20613;
padding: 3px;
width: 32px;
height: 32px;
transform: translate(16px, 16px);
}
.flipped {
transform: translate(-48px, -48px);
}
.iconContainer:after {
content: '';
background: linear-gradient(135deg, #e20713, #00000000);
clip-path: polygon(0 0, 50% 100%, 100% 50%);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform: translate(-50%, -50%);
transform-origin: bottom right;
}
.flipped:after {
transform: translate(-50%, -50%) rotate(180deg);
}

View File

@ -1,52 +0,0 @@
import { useEffect, useState } from "react";
import { getIntersection, getIntersectionArea } from "../../utils";
import { Marker, Point } from "react-simple-maps";
import { Coordinates } from "@mt/common-types";
import cn from "classnames";
import styles from "./TramMarker.module.css";
import { Icons, useMapWidgetContext } from "@mt/components";
interface TramMarkerProps {
coordinates: Point;
nextStopPoint: Coordinates;
}
export const TramMarker = ({ coordinates, nextStopPoint }: TramMarkerProps) => {
const [flipped, setFlipped] = useState(false);
const { rotateAngle } = useMapWidgetContext();
useEffect(() => {
const tramRect = document
.getElementById("tram-marker")
?.getBoundingClientRect();
const nextStopRect = document
.getElementById(`${nextStopPoint.lat}:${nextStopPoint.lon}`)
?.getBoundingClientRect();
if (tramRect && nextStopRect) {
// prettier-ignore
const hasIntersection = getIntersection(tramRect, nextStopRect) !== null;
const intersectionArea = getIntersectionArea(tramRect, nextStopRect);
if (hasIntersection && intersectionArea > 150) {
setFlipped((isFlipped) => !isFlipped);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [coordinates]);
return (
<Marker coordinates={coordinates} id="tram-marker">
<foreignObject
className={cn(styles.iconContainer, { [styles.flipped]: flipped })}
>
<Icons.TramMarkerIcon
className={`${styles.icon} g-transform-origin__center`}
// Inverse angle to compensate map rotation
style={{ transform: `rotate(${-rotateAngle}deg)` }}
/>
</foreignObject>
</Marker>
);
};

View File

@ -1 +0,0 @@
export * from './TramMarker';

View File

@ -1,5 +0,0 @@
export * from './TramMarker';
export * from './TrackLine';
export * from './TrackStations';
export * from './TrackAttractions';
export * from './MapWidget';

View File

@ -1,2 +0,0 @@
export * from './usePassedTrackIndex';
export * from './useNearStation';

View File

@ -1,61 +0,0 @@
import { useEffect, useState } from "react";
import { StationOnMap } from "../map-widget.interface";
import { getDistance } from "../utils";
import { Coordinates } from "@mt/common-types";
const ZOOM_DISTANCE = 100;
const ON_STATION_DISTANCE = 15;
export function useNearStation(
currentPosition: Coordinates | null,
stations: StationOnMap[],
passedTrackIndex: number
) {
const [nextStation, setNextStation] = useState<StationOnMap | null>(null);
const [currentStation, setCurrentStation] = useState<StationOnMap | null>(
null
);
const [isOnStation, setIsOnStation] = useState<boolean>(false);
useEffect(() => {
if (!currentPosition) {
return;
}
const nextStationIndex = stations.findIndex(
({ pointOnMap }) => pointOnMap.trackIndex > passedTrackIndex
);
const nextStation = stations[nextStationIndex] ?? null;
const prevStation = stations[nextStationIndex - 1] ?? null;
const distanceToNext = nextStation
? getDistance(currentPosition, nextStation.pointOnMap)
: ZOOM_DISTANCE + 1;
setNextStation(nextStation);
if (distanceToNext <= ZOOM_DISTANCE) {
setCurrentStation(nextStation);
setIsOnStation(distanceToNext <= ON_STATION_DISTANCE);
return;
}
const distanceToPrev = prevStation
? getDistance(currentPosition, prevStation.pointOnMap)
: ZOOM_DISTANCE + 1;
if (distanceToPrev <= ZOOM_DISTANCE) {
setCurrentStation(prevStation);
setIsOnStation(distanceToPrev <= ON_STATION_DISTANCE);
return;
}
setCurrentStation(null);
setIsOnStation(false);
}, [currentPosition, stations, passedTrackIndex]);
return { currentStation, nextStation, isOnStation };
}

View File

@ -1,61 +0,0 @@
import { useEffect, useState } from "react";
import { Coordinates, Track } from "@mt/common-types";
import { getDistance, getPointDeviation } from "../utils";
const APPROXIMATE_DISTANCE = 15; // [meters] half of tramway length (~30 meters)
export function usePassedTrackIndex(
track: Track | null,
currentPosition: Coordinates | null
) {
const [passedTrackIndex, setPassedTrackIndex] = useState<number>(0);
useEffect(() => {
if (!track || !currentPosition) {
return;
}
let minDistance = getDistance(track[0], currentPosition);
let newPassedIndex = 0;
for (let i = 1; i < track.length; i++) {
const distance = getDistance(track[i], currentPosition);
if (distance < minDistance) {
newPassedIndex = i;
minDistance = distance;
}
}
/**
* Is current position more than APPROXIMATE_DISTANCE far from found track point
* we need to check that we really reach newPassedIndex. If not should decrement index
*/
if (
getDistance(track[newPassedIndex], currentPosition) > APPROXIMATE_DISTANCE
) {
const prevIndex = Math.max(newPassedIndex - 1, 0);
const nextIndex = Math.min(newPassedIndex + 1, track.length - 1);
const leftDeviation = getPointDeviation(
track[prevIndex],
track[newPassedIndex], // Ближайшая точка трека
currentPosition
);
const rightDeviation = getPointDeviation(
track[newPassedIndex], // Ближайшая точка трека
track[nextIndex],
currentPosition
);
if (leftDeviation >= rightDeviation) {
newPassedIndex--;
}
}
setPassedTrackIndex(newPassedIndex);
}, [track, currentPosition]);
return { passedTrackIndex };
}

View File

@ -1,5 +0,0 @@
export * from './map-widget.constant';
export * from './map-widget.interface';
export * from './components';
export * from './MapWidgetContext';
export * from './map-widget-context.interface';

View File

@ -1,50 +0,0 @@
import { Coordinates, SetState, uuid } from "@mt/common-types";
import { MapData, StationOnMap } from "@mt/components";
import { UseFormReturn } from "react-hook-form";
import { RouteStation } from "@mt/common-types";
import { GeoProjection } from "d3-geo";
export interface MapWidgetContextType {
// External data
track: MapData["trackPoints"] | null;
stations: MapData["stationsOnMap"];
attractionGroups: MapData["touristAttractionGroupsOnMap"];
rotateAngle: MapData["mapRotateAngle"];
center: Coordinates;
projection: GeoProjection;
currentPosition: Coordinates | null;
middleTrackCoordinates: Coordinates | null;
passedTrackIndex: number;
currentStation: StationOnMap | null;
nextStation: StationOnMap | null;
isOnStation: boolean;
// Calculated data
setCurrentPosition: SetState<Coordinates | null>;
isDragMode: boolean;
setIsDragMode: SetState<boolean>;
isEditMode: boolean;
setIsEditMode: SetState<boolean>;
onMapDataFetched: (payload: MapData) => void;
settingsForm: UseFormReturn<MapSettings>;
onSettingsFormChange: () => void;
onMapCenterMoved: (center: Coordinates) => void;
onStationUpdate: (
stationId: uuid,
data: Partial<Pick<StationOnMap, "labelAlignment" | "labelOffset">>
) => void;
getUpdatedStations: () => Partial<RouteStation>;
isMapDataChanged: boolean;
}
export interface MapSettings {
rotateAngle: number;
fullScale: number;
zoomedScale: number;
center: Coordinates;
currentStationId?: string;
}

View File

@ -1,10 +0,0 @@
import { MapSettings } from "./map-widget-context.interface";
export const zeroCoordinates = { lat: 0, lon: 0 };
export const EMPTY_SETTING_VALUE: MapSettings = {
rotateAngle: 0,
center: zeroCoordinates,
fullScale: 0,
zoomedScale: 0,
};

View File

@ -1,38 +0,0 @@
import {
AttractionGroupIconSizeType,
Coordinates,
StationOnMap as StationOnMapBase,
Track,
uuid,
Transfer,
} from "@mt/common-types";
export type PointOnTrack = Coordinates & {
trackIndex: number;
};
export type StationOnMap = StationOnMapBase & {
pointOnMap: PointOnTrack;
transferStationInfos: Transfer[];
};
export interface AttractionOnMap {
id: uuid;
pointOnMap: Coordinates;
}
export interface AttractionGroup {
iconSize: AttractionGroupIconSizeType;
pointOnMap: Coordinates;
touristAttractionsOnMap: AttractionOnMap[];
}
export interface MapData {
mapRotateAngle: number;
fullMapScale: number;
zoomedMapScale: number;
centerOfMapPoint: Coordinates;
trackPoints: Track;
stationsOnMap: StationOnMap[];
touristAttractionGroupsOnMap: AttractionGroup[];
}

View File

@ -1,23 +0,0 @@
import { Coordinates } from '@mt/common-types';
import { getDistance } from './get-distance';
/**
* This function return deviation of point form the passed straight line
* If deviation equals 0 this means the point lay on the line
* otherwise don't and we can draw a triangle by this 3 point
* @param begin: Point
* @param end: Point
* @param point: Point
* @returns deviation: number
*/
export function getPointDeviation(
begin: Coordinates,
end: Coordinates,
point: Coordinates
): number {
const distanceBtw = getDistance(begin, end);
const distanceTo = getDistance(begin, point);
const distanceFrom = getDistance(point, end);
return distanceBtw - (distanceFrom + distanceTo);
}

View File

@ -1,28 +0,0 @@
import { Coordinates } from '@mt/common-types';
const EARTH_RADIUS = 6372795; // meters
export function getDistance(a: Coordinates, b: Coordinates): number {
const { PI, sin, cos, pow, sqrt, atan2 } = Math;
const aRad = {
lat: (a.lat * PI) / 180,
lon: (a.lon * PI) / 180,
};
const bRad = {
lat: (b.lat * PI) / 180,
lon: (b.lon * PI) / 180,
};
const delta = bRad.lon - aRad.lon;
// вычисления длины большого круга
const y = sqrt(
pow(cos(bRad.lat) * sin(delta), 2) +
pow(cos(aRad.lat) * sin(bRad.lat) - sin(aRad.lat) * cos(bRad.lat) * cos(delta), 2)
);
const x = sin(aRad.lat) * sin(bRad.lat) + cos(aRad.lat) * cos(bRad.lat) * cos(delta);
return +(atan2(y, x) * EARTH_RADIUS).toFixed(2);
}

View File

@ -1,6 +0,0 @@
import { Coordinates } from '@mt/common-types';
import { Point } from 'react-simple-maps';
export function getMapPoint({ lat, lon }: Coordinates): Point {
return [lon, lat];
}

View File

@ -1,4 +0,0 @@
export * from './get-deviation';
export * from './get-distance';
export * from './get-map-point';
export * from './intersections';

View File

@ -1,30 +0,0 @@
interface Rectangle {
left: number;
top: number;
right: number;
bottom: number;
}
export function getIntersection(rect1: Rectangle, rect2: Rectangle): Rectangle | null {
const left = Math.max(rect1.left, rect2.left);
const top = Math.max(rect1.top, rect2.top);
const right = Math.min(rect1.right, rect2.right);
const bottom = Math.min(rect1.bottom, rect2.bottom);
if (left < right && top < bottom) {
return { left, top, right, bottom };
}
return null;
}
export function getIntersectionArea(rect1: Rectangle, rect2: Rectangle): number {
const intersection = getIntersection(rect1, rect2);
if (intersection === null) {
return 0;
}
const width = intersection.right - intersection.left;
const height = intersection.bottom - intersection.top;
return width * height;
}

View File

@ -1,7 +0,0 @@
export const MyComponent = () => {
return (
<div style={{ width: "100px", height: "100px", backgroundColor: "red" }}>
MyComponent
</div>
);
};

View File

@ -1,67 +0,0 @@
.root {
display: flex;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
rgba(179, 165, 152, 0.4);
box-shadow: inset 4px 4px 12px rgba(255, 255, 255, 0.12);
border-radius: 10px;
}
.number {
width: 96px;
height: 96px;
background: #fcd500;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 92px;
font-weight: 900;
}
.content {
width: 265px;
height: 96px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 8px;
}
.title {
white-space: nowrap;
overflow: hidden;
}
.crawlLine {
display: inline-block;
animation: crawl linear infinite;
animation-duration: 10s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
.titleStart,
.titleEnd {
font-weight: 700;
font-size: 24px;
line-height: 28px;
color: #ffffff;
}
.titleTranslation {
margin-top: 4px;
font-weight: 400;
font-size: 12px;
line-height: 15px;
color: #cbcbcb;
}
@keyframes crawl {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}

View File

@ -1,74 +0,0 @@
import React, { HTMLAttributes, useContext, useEffect, useRef } from "react";
import { LocalizationContext, LocalizedString } from "@mt/i18n";
import styles from "./RouteInfoWidget.module.css";
import cn from "classnames";
export interface RouteInfoData {
routeNumber: string;
firstStationName: LocalizedString;
lastStationName: LocalizedString;
}
interface RouteInfoWidgetProps extends HTMLAttributes<HTMLDivElement> {
routeInfo?: RouteInfoData;
}
export function RouteInfoWidget({
routeInfo,
className,
...props
}: RouteInfoWidgetProps) {
const contentContainerRef = useRef<HTMLDivElement>(null);
const titleRefs = useRef<Array<HTMLSpanElement>>([]);
const { locale } = useContext(LocalizationContext);
useEffect(() => {
if (!routeInfo?.firstStationName || !routeInfo?.lastStationName) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const container = contentContainerRef.current!;
const containerWidth = container.offsetWidth;
titleRefs.current.forEach((title) => {
const titleWidth = title.offsetWidth;
const paddingWidth = 8 * 2;
if (titleWidth + paddingWidth > containerWidth) {
title.classList.add(styles.crawlLine);
} else {
title.classList.remove(styles.crawlLine);
}
});
}, [routeInfo, titleRefs, contentContainerRef]);
return (
<div className={cn(styles.root, className)} {...props}>
<div className={styles.number}>{routeInfo?.routeNumber || "--"}</div>
{routeInfo ? (
<div className={styles.content} ref={contentContainerRef}>
<div className={cn(styles.title, styles.titleStart)}>
<span ref={(ref) => (titleRefs.current[0] = ref!)}>
{routeInfo.firstStationName.ru}
</span>
</div>
<div className={cn(styles.title, styles.titleEnd)}>
<span ref={(ref) => (titleRefs.current[1] = ref!)}>
{routeInfo.lastStationName.ru}
</span>
</div>
<div className={cn(styles.title, styles.titleTranslation)}>
<span ref={(ref) => (titleRefs.current[2] = ref!)}>
{locale === "zh"
? `${routeInfo.firstStationName.zh} ${routeInfo.lastStationName.zh}`
: `${routeInfo.firstStationName.en} ${routeInfo.lastStationName.en}`}
</span>
</div>
</div>
) : null}
</div>
);
}

View File

@ -1,24 +0,0 @@
.root {
position: relative;
touch-action: none;
overflow-y: hidden !important;
}
.scrollbar {
--scrollbar-min-height: 10px;
--scrollbar-height: var(--scrollbar-min-height);
--scrollbar-offset: 0px;
--scrollbar-visibility: hidden;
position: absolute;
top: 0;
right: 0;
width: 3px;
border-radius: 6px;
z-index: 1;
background-color: #ffffff;
min-height: var(--scrollbar-min-height);
height: var(--scrollbar-height);
visibility: var(--scrollbar-visibility);
transform: translateY(var(--scrollbar-offset));
}

View File

@ -1,142 +0,0 @@
import {
HTMLAttributes,
PointerEvent,
WheelEvent,
useEffect,
useRef,
useState,
} from "react";
import cn from "classnames";
import styles from "./TouchScrollWrapper.module.css";
import { useCssProperty } from "@mt/utils";
const getNumberPxFormatter = (numberStr: string | null) =>
Number(numberStr?.replace(/px$/, ""));
const setNumberPxFormatter = (number: number) => `${number}px`;
const { abs, min } = Math;
export const TouchScrollWrapper = ({
className,
children,
...props
}: HTMLAttributes<HTMLDivElement>) => {
const contentRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const scrollbarRef = useRef<HTMLDivElement>(null);
const scrollbarHeight = useCssProperty<number>(
"--scrollbar-height",
scrollbarRef,
setNumberPxFormatter,
getNumberPxFormatter
);
const scrollbarVisibility = useCssProperty<string>(
"--scrollbar-visibility",
scrollbarRef
);
const scrollbarOffset = useCssProperty<number>(
"--scrollbar-offset",
scrollbarRef,
setNumberPxFormatter
);
const [contentHeight, setContentHeight] = useState<number>(0);
const [containerHeight, setContainerHeight] = useState<number>(0);
const [startSwipeY, setStartSwipeY] = useState<number>(0);
const [startContentOffset, setStartContentOffset] = useState<number>(0);
const [pointerId, setPointerId] = useState(0);
useEffect(() => {
const containerEl = containerRef?.current;
const contentEl = contentRef?.current;
if (!(containerEl && contentEl)) return;
const observer = new ResizeObserver(() => {
setContainerHeight(containerEl.offsetHeight);
setContentHeight(containerEl.scrollHeight);
});
observer.observe(containerEl);
observer.observe(contentEl);
return () => {
observer.disconnect();
};
}, []);
useEffect(() => {
if (containerHeight >= contentHeight) {
scrollbarVisibility.value = "hidden";
} else {
scrollbarHeight.value =
(containerHeight / contentHeight) * containerHeight + 1;
scrollbarVisibility.value = "visible";
}
}, [contentHeight, containerHeight]);
const handlePointerDown = (e: PointerEvent) => {
setStartSwipeY(e.clientY);
setStartContentOffset(containerRef?.current?.scrollTop || 0);
};
const handleTouchScroll = (e: PointerEvent) => {
const swipeDistance = startSwipeY - e.clientY;
if (
e.pointerType !== "touch" ||
containerHeight >= contentHeight ||
(pointerId && pointerId !== e.pointerId) ||
abs(swipeDistance) < 2
) {
return;
}
setPointerId(e.pointerId);
containerRef?.current?.scrollTo(0, startContentOffset + swipeDistance);
};
const handleScroll = (e: WheelEvent) => {
if (containerHeight >= contentHeight) {
return;
}
containerRef?.current?.scrollBy(0, e.deltaY);
};
const capturePointerUp = (e: PointerEvent) => {
if (pointerId) {
e.preventDefault();
e.stopPropagation();
setPointerId(0);
}
};
const captureScroll = () => {
const { scrollTop } = containerRef.current as HTMLDivElement;
const barHeight = (scrollbarHeight.value as number) + 1; // plus 1 safety px!;
const barOffset = (scrollTop * barHeight) / containerHeight;
const maxBarOffset = containerHeight - barHeight;
scrollbarOffset.value = scrollTop + min(barOffset, maxBarOffset);
};
return (
<div
className={cn(styles.root, className)}
ref={containerRef}
onPointerDown={handlePointerDown}
onPointerMove={handleTouchScroll}
onPointerUpCapture={capturePointerUp}
onWheel={handleScroll}
onScroll={captureScroll}
{...props}
>
<div className={styles.scrollbar} ref={scrollbarRef} />
<div ref={contentRef}>{children}</div>
</div>
);
};

View File

@ -1,24 +0,0 @@
import { HTMLAttributes, ReactNode } from "react";
import { Icons } from "../Icons";
import { TransportType } from "@mt/common-types";
const transportStopIcons: Record<TransportType, ReactNode> = {
METRO_RED: <Icons.SubwayIcon width="18" height="18" fill="#E52629" />,
METRO_BLUE: <Icons.SubwayIcon width="18" height="18" fill="#2D3B8E" />,
METRO_GREEN: <Icons.SubwayIcon width="18" height="18" fill="#056939" />,
METRO_ORANGE: <Icons.SubwayIcon width="18" height="18" fill="#EB5C2C" />,
METRO_PURPLE: <Icons.SubwayIcon width="18" height="18" fill="#64328A" />,
TRAM: <Icons.TramIcon width="18" height="18" />,
TRAIN: <Icons.TrainIcon width="18" height="18" />,
TROLLEY: <Icons.TrolleyIcon width="18" height="18" />,
BUS: <Icons.BusIcon width="18" height="18" />,
};
export interface TransportIconProps extends HTMLAttributes<HTMLDivElement> {
type: TransportType;
}
export function TransportIcon({ type, ...props }: TransportIconProps) {
return <div {...props}>{transportStopIcons[type]}</div>;
}

View File

@ -1,16 +0,0 @@
export const DetHumidity = () => {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M32 63.68C19.42 63.68 9.19 53.45 9.19 40.87C9.19 28.25 22.63 9.87001 28.41 2.56001C29.28 1.45001 30.59 0.820007 32 0.820007C33.41 0.820007 34.72 1.45001 35.59 2.56001C41.37 9.88001 54.81 28.26 54.81 40.87C54.81 53.44 44.58 63.68 32 63.68ZM32 4.81001C31.9 4.81001 31.7 4.84001 31.55 5.03001C27.24 10.48 13.19 29.18 13.19 40.86C13.19 51.23 21.63 59.67 32 59.67C42.37 59.67 50.81 51.23 50.81 40.86C50.81 29.18 36.76 10.48 32.46 5.03001C32.3 4.84001 32.1 4.81001 32 4.81001Z"
fill="white"
/>
</svg>
);
};

View File

@ -1,24 +0,0 @@
export const DetWind = () => {
return (
<svg
width="64"
height="64"
viewBox="0 0 64 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22.03 23.32H4.59998C3.49998 23.32 2.59998 22.42 2.59998 21.32C2.59998 20.22 3.49998 19.32 4.59998 19.32H22.02C26 19.32 29.24 16.08 29.24 12.1C29.24 8.12001 26 4.88 22.02 4.88C18.04 4.88 14.8 8.12001 14.8 12.1C14.8 13.2 13.9 14.1 12.8 14.1C11.7 14.1 10.8 13.2 10.8 12.1C10.8 5.91001 15.84 0.880005 22.02 0.880005C28.2 0.880005 33.24 5.92001 33.24 12.1C33.24 18.28 28.21 23.32 22.03 23.32Z"
fill="white"
/>
<path
d="M50.17 34.3H14.15C13.05 34.3 12.15 33.4 12.15 32.3C12.15 31.2 13.05 30.3 14.15 30.3H50.17C54.15 30.3 57.39 27.06 57.39 23.08C57.39 19.1 54.15 15.86 50.17 15.86C46.19 15.86 42.95 19.1 42.95 23.08C42.95 24.18 42.05 25.08 40.95 25.08C39.85 25.08 38.95 24.18 38.95 23.08C38.95 16.89 43.99 11.86 50.17 11.86C56.36 11.86 61.39 16.9 61.39 23.08C61.39 29.26 56.36 34.3 50.17 34.3Z"
fill="white"
/>
<path
d="M40.63 63.13C34.44 63.13 29.41 58.09 29.41 51.91C29.41 50.81 30.31 49.91 31.41 49.91C32.51 49.91 33.41 50.81 33.41 51.91C33.41 55.89 36.65 59.13 40.63 59.13C44.61 59.13 47.85 55.89 47.85 51.91C47.85 47.93 44.61 44.69 40.63 44.69H4.59998C3.49998 44.69 2.59998 43.79 2.59998 42.69C2.59998 41.59 3.49998 40.69 4.59998 40.69H40.62C46.81 40.69 51.84 45.73 51.84 51.91C51.84 58.09 46.82 63.13 40.63 63.13Z"
fill="white"
/>
</svg>
);
};

View File

@ -1,3 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.15 14.23C46.84 14.23 46.53 14.24 46.23 14.26C44.81 14.34 43.42 13.73 42.63 12.54C39.6 7.98 34.51 5 28.75 5C21.58 5 15.45 9.63 13.02 16.15C12.62 17.24 11.72 18.08 10.61 18.44C4.47 20.42 0 26.35 0 33.36C0 42 6.78 49 15.15 49H47.15C56.46 49 64 41.22 64 31.61C64 22.01 56.46 14.23 47.15 14.23Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 422 B

View File

@ -1,4 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M49.0191 29.96C57.2891 29.96 63.9991 23.25 63.9991 14.98C63.9991 6.71 57.2891 0 49.0191 0C40.7491 0 34.0391 6.71 34.0391 14.98C34.0291 23.26 40.7391 29.96 49.0191 29.96Z" fill="#FCD500"/>
<path d="M43.4668 17.3834C43.1948 17.3834 42.9128 17.3933 42.6408 17.4033C41.2708 17.4728 39.9713 16.8073 39.1957 15.6948C36.3852 11.6422 31.7514 9 26.5032 9C19.9655 9 14.3748 13.1122 12.078 18.933C11.6348 20.0554 10.7181 20.8997 9.56975 21.2871C4.00922 23.1545 0 28.4984 0 34.7859C0 42.633 6.25559 49 13.9718 49H43.4668C52.0393 49 59 41.9277 59 33.1967C59 24.4656 52.0493 17.3834 43.4668 17.3834Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 714 B

View File

@ -1,7 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.15 11.22C46.84 11.22 46.53 11.23 46.23 11.25C44.81 11.33 43.42 10.72 42.63 9.53C39.59 4.98 34.51 2 28.75 2C21.58 2 15.45 6.63 13.02 13.15C12.62 14.24 11.72 15.08 10.61 15.44C4.47 17.41 0 23.35 0 30.35C0 38.99 6.78 45.99 15.15 45.99H47.15C56.45 45.99 64 38.21 64 28.6C64 19 56.46 11.22 47.15 11.22Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6908 48.5015C18.4079 48.9162 18.6532 49.8337 18.2385 50.5509L14.4685 57.0709C14.0538 57.7881 13.1362 58.0333 12.4191 57.6186C11.7019 57.2039 11.4567 56.2864 11.8714 55.5692L15.6414 49.0492C16.0561 48.332 16.9736 48.0868 17.6908 48.5015Z" fill="#00B1FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.8607 53.2215C26.5779 53.6362 26.8231 54.5537 26.4084 55.2709L22.6384 61.7909C22.2237 62.508 21.3062 62.7533 20.589 62.3386C19.8718 61.9239 19.6266 61.0063 20.0413 60.2892L23.8113 53.7692C24.226 53.052 25.1435 52.8068 25.8607 53.2215Z" fill="#00B1FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M39.1988 48.9706C39.9165 49.3845 40.1627 50.3017 39.7489 51.0194L35.9889 57.5394C35.575 58.257 34.6578 58.5033 33.9401 58.0894C33.2225 57.6756 32.9762 56.7583 33.39 56.0407L37.1501 49.5207C37.5639 48.803 38.4812 48.5568 39.1988 48.9706Z" fill="#00B1FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.3687 53.6906C48.0864 54.1044 48.3326 55.0217 47.9188 55.7394L44.1588 62.2593C43.7449 62.977 42.8277 63.2233 42.11 62.8094C41.3924 62.3955 41.1461 61.4783 41.56 60.7606L45.32 54.2406C45.7338 53.523 46.6511 53.2767 47.3687 53.6906Z" fill="#00B1FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,11 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.95 0.190002C33.0545 0.190002 33.95 1.08543 33.95 2.19V61.87C33.95 62.9746 33.0545 63.87 31.95 63.87C30.8454 63.87 29.95 62.9746 29.95 61.87V2.19C29.95 1.08543 30.8454 0.190002 31.95 0.190002Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.4458 4.92578C21.2268 4.14473 22.4932 4.14473 23.2742 4.92578L32.0107 13.6623L40.6265 5.05508C41.4079 4.27442 42.6742 4.27505 43.4549 5.05649C44.2356 5.83793 44.2349 7.10426 43.4535 7.88491L32.0093 19.3177L20.4458 7.75421C19.6647 6.97316 19.6647 5.70683 20.4458 4.92578Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.89 45.0416L43.4542 56.6058C44.2352 57.3868 44.2352 58.6532 43.4542 59.4342C42.6732 60.2153 41.4068 60.2153 40.6258 59.4342L31.89 50.6984L23.2742 59.3142C22.4932 60.0953 21.2268 60.0953 20.4458 59.3142C19.6647 58.5332 19.6647 57.2668 20.4458 56.4858L31.89 45.0416Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.36797 16.1101C4.92021 15.1535 6.14337 14.8257 7.09998 15.3779L58.79 45.2179C59.7466 45.7701 60.0744 46.9933 59.5222 47.9499C58.9699 48.9065 57.7468 49.2343 56.7901 48.6821L5.10015 18.8421C4.14354 18.2899 3.81573 17.0667 4.36797 16.1101Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3312 8.57847C15.398 8.29193 16.495 8.92441 16.7816 9.99117L20.981 25.6247L5.17684 29.8521C4.10978 30.1375 3.01339 29.5039 2.72797 28.4368C2.44255 27.3697 3.07619 26.2733 4.14324 25.9879L16.0791 22.7953L12.9185 11.0288C12.632 9.96208 13.2645 8.86502 14.3312 8.57847Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.6571 8.51802C50.7242 8.80363 51.3576 9.90015 51.072 10.9672L47.8787 22.8969L59.6488 26.0585C60.7156 26.345 61.3481 27.4421 61.0615 28.5088C60.775 29.5756 59.6779 30.2081 58.6112 29.9215L42.9813 25.7232L47.208 9.93286C47.4936 8.86585 48.5901 8.23241 49.6571 8.51802Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.56812 35.7025C2.85394 34.6355 3.95058 34.0023 5.01753 34.2881L20.65 38.4758L16.4117 54.2781C16.1256 55.345 15.0288 55.9779 13.9619 55.6917C12.895 55.4056 12.2621 54.3088 12.5483 53.2419L15.75 41.3042L3.98249 38.1519C2.91554 37.8661 2.28231 36.7694 2.56812 35.7025Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.4318 35.7619C61.7179 36.8288 61.085 37.9256 60.0181 38.2117L48.0793 41.4138L51.2319 53.1825C51.5177 54.2494 50.8845 55.3461 49.8176 55.6319C48.7506 55.9177 47.654 55.2845 47.3681 54.2175L43.1808 38.5862L58.9819 34.3483C60.0488 34.0621 61.1456 34.695 61.4318 35.7619Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.5222 16.1101C60.0744 17.0667 59.7466 18.2899 58.79 18.8421L7.09998 48.6821C6.14337 49.2343 4.92021 48.9065 4.36797 47.9499C3.81573 46.9933 4.14354 45.7701 5.10015 45.2179L56.7901 15.3779C57.7468 14.8257 58.9699 15.1535 59.5222 16.1101Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,11 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.15 10.22C46.84 10.22 46.53 10.23 46.23 10.25C44.81 10.33 43.42 9.72 42.63 8.53C39.59 3.98 34.51 1 28.75 1C21.58 1 15.45 5.63 13.02 12.15C12.62 13.24 11.72 14.08 10.61 14.44C4.47 16.42 0 22.35 0 29.36C0 38 6.78 45 15.15 45H47.15C56.45 45 64 37.22 64 27.61C64 18.01 56.46 10.22 47.15 10.22Z" fill="white"/>
<path d="M34.6995 58.07C35.5334 58.07 36.2095 57.394 36.2095 56.56C36.2095 55.7261 35.5334 55.05 34.6995 55.05C33.8655 55.05 33.1895 55.7261 33.1895 56.56C33.1895 57.394 33.8655 58.07 34.6995 58.07Z" fill="white"/>
<path d="M38.4397 51.5901C39.2736 51.5901 39.9497 50.914 39.9497 50.0801C39.9497 49.2461 39.2736 48.5701 38.4397 48.5701C37.6057 48.5701 36.9297 49.2461 36.9297 50.0801C36.9297 50.914 37.6057 51.5901 38.4397 51.5901Z" fill="white"/>
<path d="M42.8694 62.82C43.7033 62.82 44.3794 62.144 44.3794 61.31C44.3794 60.4761 43.7033 59.8 42.8694 59.8C42.0354 59.8 41.3594 60.4761 41.3594 61.31C41.3594 62.144 42.0354 62.82 42.8694 62.82Z" fill="white"/>
<path d="M46.6096 56.3401C47.4436 56.3401 48.1196 55.664 48.1196 54.8301C48.1196 53.9961 47.4436 53.3201 46.6096 53.3201C45.7757 53.3201 45.0996 53.9961 45.0996 54.8301C45.0996 55.664 45.7757 56.3401 46.6096 56.3401Z" fill="white"/>
<path d="M13.1799 58.07C14.0139 58.07 14.6899 57.394 14.6899 56.56C14.6899 55.7261 14.0139 55.05 13.1799 55.05C12.346 55.05 11.6699 55.7261 11.6699 56.56C11.6699 57.394 12.346 58.07 13.1799 58.07Z" fill="white"/>
<path d="M16.9202 51.5901C17.7541 51.5901 18.4302 50.914 18.4302 50.0801C18.4302 49.2461 17.7541 48.5701 16.9202 48.5701C16.0862 48.5701 15.4102 49.2461 15.4102 50.0801C15.4102 50.914 16.0862 51.5901 16.9202 51.5901Z" fill="white"/>
<path d="M21.3498 62.82C22.1838 62.82 22.8598 62.144 22.8598 61.31C22.8598 60.4761 22.1838 59.8 21.3498 59.8C20.5159 59.8 19.8398 60.4761 19.8398 61.31C19.8398 62.144 20.5159 62.82 21.3498 62.82Z" fill="white"/>
<path d="M25.0901 56.3401C25.924 56.3401 26.6001 55.664 26.6001 54.8301C26.6001 53.9961 25.924 53.3201 25.0901 53.3201C24.2561 53.3201 23.5801 53.9961 23.5801 54.8301C23.5801 55.664 24.2561 56.3401 25.0901 56.3401Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,18 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_383_344)">
<path d="M19.3993 21.64C18.8293 21.64 18.2593 21.42 17.8193 20.99L9.3693 12.53C8.4993 11.66 8.4993 10.25 9.3693 9.38004C10.2393 8.51004 11.6493 8.51004 12.5193 9.38004L20.9693 17.83C21.8393 18.7 21.8393 20.11 20.9693 20.98C20.5393 21.42 19.9693 21.64 19.3993 21.64Z" fill="#FCD500"/>
<path d="M61.7701 34.24H49.8101C48.5801 34.24 47.5801 33.24 47.5801 32.01C47.5801 30.78 48.5801 29.78 49.8101 29.78H61.7701C63.0001 29.78 64.0001 30.78 64.0001 32.01C64.0001 33.24 63.0001 34.24 61.7701 34.24Z" fill="#FCD500"/>
<path d="M44.5997 21.64C44.0297 21.64 43.4597 21.42 43.0197 20.99C42.1497 20.12 42.1497 18.71 43.0197 17.84L51.4697 9.39005C52.3397 8.52005 53.7497 8.52005 54.6197 9.39005C55.4897 10.26 55.4897 11.67 54.6197 12.54L46.1697 20.99C45.7397 21.42 45.1697 21.64 44.5997 21.64Z" fill="#FCD500"/>
<path d="M31.9995 16.42C30.7695 16.42 29.7695 15.42 29.7695 14.19V2.23C29.7695 1 30.7695 0 31.9995 0C33.2295 0 34.2295 1 34.2295 2.23V14.19C34.2295 15.42 33.2295 16.42 31.9995 16.42Z" fill="#FCD500"/>
<path d="M14.19 34.24H2.24C1 34.24 0 33.24 0 32.01C0 30.78 1 29.78 2.23 29.78H14.18C15.41 29.78 16.41 30.78 16.41 32.01C16.41 33.24 15.42 34.24 14.19 34.24Z" fill="#FCD500"/>
<path d="M10.9493 55.29C10.3793 55.29 9.8093 55.0699 9.3693 54.6399C8.4993 53.7699 8.4993 52.36 9.3693 51.49L17.8193 43.04C18.6893 42.17 20.0993 42.17 20.9693 43.04C21.8393 43.91 21.8393 45.3199 20.9693 46.1899L12.5193 54.6399C12.0893 55.0699 11.5193 55.29 10.9493 55.29Z" fill="#FCD500"/>
<path d="M31.9995 64.0098C30.7695 64.0098 29.7695 63.0098 29.7695 61.7798V49.8198C29.7695 48.5898 30.7695 47.5898 31.9995 47.5898C33.2295 47.5898 34.2295 48.5898 34.2295 49.8198V61.7798C34.2295 63.0098 33.2295 64.0098 31.9995 64.0098Z" fill="#FCD500"/>
<path d="M53.0497 55.29C52.4797 55.29 51.9097 55.0699 51.4697 54.6399L43.0197 46.1899C42.1497 45.3199 42.1497 43.91 43.0197 43.04C43.8897 42.17 45.2997 42.17 46.1697 43.04L54.6197 51.49C55.4897 52.36 55.4897 53.7699 54.6197 54.6399C54.1897 55.0699 53.6197 55.29 53.0497 55.29Z" fill="#FCD500"/>
<path d="M32 43.9299C38.6252 43.9299 44 38.5551 44 31.9299C44 25.3047 38.6252 19.9299 32 19.9299C25.3748 19.9299 20 25.3047 20 31.9299C20 38.5551 25.3748 43.9299 32 43.9299Z" fill="#FCD500"/>
</g>
<defs>
<clipPath id="clip0_383_344">
<rect width="64" height="64" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,4 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.15 12.22C46.84 12.22 46.53 12.23 46.23 12.25C44.81 12.33 43.42 11.72 42.63 10.53C39.59 5.98 34.51 3 28.75 3C21.58 3 15.45 7.63 13.02 14.15C12.62 15.24 11.72 16.08 10.61 16.44C4.47 18.42 0 24.35 0 31.35C0 39.99 6.78 46.99 15.15 46.99H47.15C56.45 46.99 64 39.21 64 29.6C64 20 56.46 12.22 47.15 12.22Z" fill="white"/>
<path d="M26.0996 47.0101L47.6396 22.1001L44.5296 36.8901H56.9796L33.3696 61.8001L37.0596 47.0101H26.0996Z" fill="#FCD500"/>
</svg>

Before

Width:  |  Height:  |  Size: 556 B

View File

@ -1,3 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M32 63.68C19.42 63.68 9.19 53.45 9.19 40.87C9.19 28.25 22.63 9.87001 28.41 2.56001C29.28 1.45001 30.59 0.820007 32 0.820007C33.41 0.820007 34.72 1.45001 35.59 2.56001C41.37 9.88001 54.81 28.26 54.81 40.87C54.81 53.44 44.58 63.68 32 63.68ZM32 4.81001C31.9 4.81001 31.7 4.84001 31.55 5.03001C27.24 10.48 13.19 29.18 13.19 40.86C13.19 51.23 21.63 59.67 32 59.67C42.37 59.67 50.81 51.23 50.81 40.86C50.81 29.18 36.76 10.48 32.46 5.03001C32.3 4.84001 32.1 4.81001 32 4.81001Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 599 B

View File

@ -1,5 +0,0 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.03 23.32H4.59998C3.49998 23.32 2.59998 22.42 2.59998 21.32C2.59998 20.22 3.49998 19.32 4.59998 19.32H22.02C26 19.32 29.24 16.08 29.24 12.1C29.24 8.12001 26 4.88 22.02 4.88C18.04 4.88 14.8 8.12001 14.8 12.1C14.8 13.2 13.9 14.1 12.8 14.1C11.7 14.1 10.8 13.2 10.8 12.1C10.8 5.91001 15.84 0.880005 22.02 0.880005C28.2 0.880005 33.24 5.92001 33.24 12.1C33.24 18.28 28.21 23.32 22.03 23.32Z" fill="white"/>
<path d="M50.17 34.3H14.15C13.05 34.3 12.15 33.4 12.15 32.3C12.15 31.2 13.05 30.3 14.15 30.3H50.17C54.15 30.3 57.39 27.06 57.39 23.08C57.39 19.1 54.15 15.86 50.17 15.86C46.19 15.86 42.95 19.1 42.95 23.08C42.95 24.18 42.05 25.08 40.95 25.08C39.85 25.08 38.95 24.18 38.95 23.08C38.95 16.89 43.99 11.86 50.17 11.86C56.36 11.86 61.39 16.9 61.39 23.08C61.39 29.26 56.36 34.3 50.17 34.3Z" fill="white"/>
<path d="M40.63 63.13C34.44 63.13 29.41 58.09 29.41 51.91C29.41 50.81 30.31 49.91 31.41 49.91C32.51 49.91 33.41 50.81 33.41 51.91C33.41 55.89 36.65 59.13 40.63 59.13C44.61 59.13 47.85 55.89 47.85 51.91C47.85 47.93 44.61 44.69 40.63 44.69H4.59998C3.49998 44.69 2.59998 43.79 2.59998 42.69C2.59998 41.59 3.49998 40.69 4.59998 40.69H40.62C46.81 40.69 51.84 45.73 51.84 51.91C51.84 58.09 46.82 63.13 40.63 63.13Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
export * from './weather-widget';
export * from './weather.interface';
export * from './weather.constant';

View File

@ -1,44 +0,0 @@
import { createElement } from "react";
import IconCloudy from "./icons/cond_cloudy.svg";
import IconRainy from "./icons/cond_rainy.svg";
import IconPartlyCloudy from "./icons/cond_partlycloudy.svg";
import IconSnow from "./icons/cond_snow.svg";
import IconSnowy from "./icons/cond_snowy.svg";
import IconSunny from "./icons/cond_sunny.svg";
import IconThunder from "./icons/cond_thunder.svg";
interface WeatherWidgetIconProps {
icon: string | null;
size: number;
}
const Icons = {
CLOUDY: IconCloudy,
RAINY: IconRainy,
PARTLYCLOUDY: IconPartlyCloudy,
SNOW: IconSnow,
SNOWY: IconSnowy,
SUNNY: IconSunny,
THUNDER: IconThunder,
};
export function WeatherWidgetIcon({ icon, size = 16 }: WeatherWidgetIconProps) {
const svg = Icons[icon as keyof typeof Icons] || null;
if (!svg || !icon)
return (
<div
style={{
width: `${size}px`,
height: `${size}px`,
textAlign: "center",
margin: "0 auto",
fontSize: `${size}px`,
lineHeight: 1,
overflow: "hidden",
}}
children="--"
/>
);
return createElement(svg);
}

View File

@ -1,77 +0,0 @@
import { WeatherWidgetIcon } from "./weather-widget-icon";
import IconHumidity from "./icons/det_humidity.svg";
import IconWind from "./icons/det_wind.svg";
import { WeatherDayRow, WeatherWidgetData } from "./weather.interface";
const Weekdays = ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"];
const WRow = ({ temperature, weekday, condition }: WeatherDayRow) => (
<div
style={{
display: "flex",
marginBottom: "8px",
}}
>
<div
style={{
width: 16,
height: 16,
}}
>
<WeatherWidgetIcon icon={condition} size={16} />
</div>
<div style={{ marginLeft: 8, minWidth: 22 }} children={Weekdays[weekday]} />
<div style={{ marginLeft: 8 }} children={`${temperature ?? "--"}°`} />
</div>
);
export function WeatherWidgetRight({
forecasts,
weatherInfo,
}: WeatherWidgetData) {
const wd = new Date().getDay();
return (
<div
style={{
textAlign: "center",
width: "50%",
fontSize: 18,
lineHeight: "21px",
}}
>
<div
style={{
borderBottom: "1px solid #999",
marginBottom: "8px",
marginTop: "8px",
}}
>
{[...forecasts].slice(0, 3).map((d, idx) => (
<WRow key={idx} {...d?.weatherInfo} weekday={(wd + idx + 1) % 7} />
))}
</div>
<div
style={{
display: "flex",
alignItems: "center",
marginBottom: 8,
}}
>
<IconHumidity />
<b children={weatherInfo?.humidity ?? "--"} />%
</div>
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<IconWind />
<b children={weatherInfo?.windSpeed ?? "--"} />
&nbsp;м/с
</div>
</div>
);
}

View File

@ -1,54 +0,0 @@
import { useEffect, useState } from 'react';
const add0 = (val: number) => `${val < 10 ? '0' : ''}${val}`;
const weekdays = [
'воскресенье',
'понедельник',
'вторник',
'среда',
'четверг',
'пятница',
'суббота',
'воскресенье',
];
export function WeatherWidgetTime() {
const [now, setNow] = useState(new Date());
useEffect(() => {
const interval = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(interval);
}, []);
return (
<div
style={{
textAlign: 'center',
borderBottom: '1px solid #999',
paddingBottom: '13px',
}}
>
<div
style={{
fontWeight: 600,
fontSize: '48px',
lineHeight: '56px',
}}
>
{now.getHours()}
<span children={':'} style={{ opacity: now.getSeconds() % 4 !== 3 ? 1 : 0 }} />
{add0(now.getMinutes())}
</div>
<div
style={{
fontWeight: 400,
fontSize: '14px',
lineHeight: '1',
}}
>
{now.getDate()}.{add0(now.getMonth() + 1)}, {weekdays[now.getDay()]}
</div>
</div>
);
}

View File

@ -1,48 +0,0 @@
import { WeatherWidgetIcon } from './weather-widget-icon';
import { WeatherDayProps } from './weather.interface';
const WeatherTypes = {
RAINY: 'дождь',
CLOUDY: 'облачно',
PARTLYCLOUDY: 'переменная облачность',
SNOW: 'снег',
SNOWY: 'идет снег',
SUNNY: 'солнце',
THUNDER: '',
UNKNOWN: 'неизвестно',
};
export function WeatherWidgetToday(props: WeatherDayProps) {
const { temperature, condition } = props;
const wType = WeatherTypes[condition as keyof typeof WeatherTypes] ?? WeatherTypes.UNKNOWN;
return (
<div
style={{
textAlign: 'center',
width: '50%',
}}
>
<div>
<WeatherWidgetIcon icon={condition} size={72} />
</div>
<div
style={{
fontSize: 52,
lineHeight: '61px',
fontWeight: '600',
letterSpacing: '-.075',
}}
children={`${temperature ?? '--'}°`}
/>
<div
style={{
fontSize: 16,
lineHeight: '18.75px',
}}
children={wType}
/>
</div>
);
}

View File

@ -1,46 +0,0 @@
import styled from '@emotion/styled';
import { HTMLAttributes } from 'react';
import { WeatherWidgetRight } from './weather-widget-right';
import { WeatherWidgetTime } from './weather-widget-time';
import { WeatherWidgetToday } from './weather-widget-today';
import { WeatherWidgetData } from './weather.interface';
import { WEATHER_DEFAULTS } from './weather.constant';
const StyledWeatherWidget = styled.div`
width: 225px;
height: 260px;
padding: 8px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 100%),
rgba(179, 165, 152, 0.4);
box-shadow: inset 4px 4px 12px rgba(255, 255, 255, 0.12);
backdrop-filter: blur(6px);
color: #fff;
font-weight: 400;
border-radius: 10px;
`;
interface Props extends HTMLAttributes<HTMLDivElement> {
weatherData?: WeatherWidgetData;
}
export function WeatherWidget({ weatherData = WEATHER_DEFAULTS, ...props }: Props) {
const { weatherInfo, forecasts } = weatherData;
return (
<StyledWeatherWidget {...props}>
<WeatherWidgetTime />
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '8px',
}}
>
<WeatherWidgetToday {...weatherInfo} />
<WeatherWidgetRight weatherInfo={weatherInfo} forecasts={forecasts} />
</div>
</StyledWeatherWidget>
);
}

View File

@ -1,28 +0,0 @@
export const WEATHER_DEFAULTS = {
weatherInfo: {
condition: null,
temperature: null,
humidity: null,
windSpeed: null,
},
forecasts: [
{
weatherInfo: {
condition: null,
temperature: null,
},
},
{
weatherInfo: {
condition: null,
temperature: null,
},
},
{
weatherInfo: {
condition: null,
temperature: null,
},
},
],
};

View File

@ -1,29 +0,0 @@
export type WeatherTypes =
| 'CLOUDY'
| 'PARTLYCLOUDY'
| 'RAINY'
| 'SNOW'
| 'SNOWY'
| 'SUNNY'
| 'THUNDER';
export interface WeatherDayShortProps {
condition: WeatherTypes | null;
temperature: number | null;
}
export type WeatherDayProps = WeatherDayShortProps & {
humidity: number | null;
windSpeed: number | null;
};
export interface WeatherForecastsProps {
weatherInfo: WeatherDayShortProps;
}
export interface WeatherWidgetData {
forecasts: WeatherForecastsProps[];
weatherInfo: WeatherDayProps;
}
export type WeatherDayRow = WeatherDayShortProps & {
weekday: number;
};

View File

@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Some files were not shown because too many files have changed in this diff Show More