fix player
add translates add status add shared links add link page refactor
This commit is contained in:
parent
fecd30df70
commit
26c5c6504a
@ -35,6 +35,7 @@
|
||||
"monaco-yaml": "^5.1.1",
|
||||
"oidc-client-ts": "^2.2.4",
|
||||
"react": "^18.2.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-oidc-context": "^2.2.2",
|
||||
"react-router-dom": "^6.14.1",
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="Multi Frigate web app for NVR systems Frigate"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.svg" />
|
||||
<!--
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { hasAuthParams, useAuth } from 'react-oidc-context';
|
||||
import CenterLoader from './shared/components/CenterLoader';
|
||||
import CenterLoader from './shared/components/loaders/CenterLoader';
|
||||
import { ColorScheme, ColorSchemeProvider, MantineProvider } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import FullImageModal from './shared/components/FullImageModal';
|
||||
import AppBody from './AppBody';
|
||||
import FullProductModal from './shared/components/FullProductModal';
|
||||
import Forbidden from './pages/403';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
@ -64,8 +62,6 @@ function App() {
|
||||
}}
|
||||
>
|
||||
<BrowserRouter>
|
||||
<FullImageModal />
|
||||
<FullProductModal />
|
||||
<AppBody />
|
||||
</BrowserRouter>
|
||||
</MantineProvider >
|
||||
|
||||
@ -6,6 +6,7 @@ import AppRouter from './router/AppRouter';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Context } from '.';
|
||||
import SideBar from './shared/components/SideBar';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
@ -16,9 +17,7 @@ const queryClient = new QueryClient({
|
||||
})
|
||||
|
||||
const AppBody = () => {
|
||||
useEffect(() => {
|
||||
console.log("render Main")
|
||||
})
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
|
||||
const [leftSideBar, setLeftSidebar] = useState(false)
|
||||
const [rightSideBar, setRightSidebar] = useState(false)
|
||||
@ -33,13 +32,14 @@ const AppBody = () => {
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
console.log("render Main")
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppShell
|
||||
styles={{
|
||||
main: {
|
||||
paddingLeft: !leftSideBar ? "1em" : '',
|
||||
paddingRight: !rightSideBar ? '3em' : '',
|
||||
paddingLeft: !leftSideBar ? "1rem" : '',
|
||||
paddingRight: !rightSideBar ? '1rem' : '',
|
||||
background: theme.colorScheme === 'dark' ? theme.colors.dark[8] : undefined,
|
||||
},
|
||||
}}
|
||||
@ -50,7 +50,8 @@ const AppBody = () => {
|
||||
<HeaderAction links={headerLinks} />
|
||||
}
|
||||
aside={
|
||||
<SideBar isHidden={rightSideBarIsHidden} side="right" />
|
||||
!sideBarsStore.rightVisible ? <></> :
|
||||
<SideBar isHidden = { rightSideBarIsHidden } side = "right" />
|
||||
}
|
||||
>
|
||||
<AppRouter />
|
||||
@ -59,4 +60,4 @@ const AppBody = () => {
|
||||
)
|
||||
};
|
||||
|
||||
export default AppBody;
|
||||
export default observer(AppBody);
|
||||
@ -1,7 +1,7 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import { Flex, Group, Select, Text } from '@mantine/core';
|
||||
import OneSelectFilter, { OneSelectItem } from '../shared/components/filters.aps/OneSelectFilter';
|
||||
@ -9,6 +9,7 @@ import { useMediaQuery } from '@mantine/hooks';
|
||||
import { dimensions } from '../shared/dimensions/dimensions';
|
||||
import CamerasTransferList from '../shared/components/CamerasTransferList';
|
||||
import { Context } from '..';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
|
||||
const AccessSettings = () => {
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
@ -39,7 +40,7 @@ const AccessSettings = () => {
|
||||
console.log('AccessSettings rendered')
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column'>
|
||||
<Text align='center' size='xl'>Please select role</Text>
|
||||
<Text align='center' size='xl'>{strings.pleaseSelectRole}</Text>
|
||||
<Flex justify='space-between' align='center' w='100%'>
|
||||
{!isMobile ? <Group w='40%' /> : <></>}
|
||||
<Select
|
||||
|
||||
@ -3,7 +3,7 @@ import FrigateHostsTable from '../widgets/FrigateHostsTable';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { deleteFrigateHostSchema, GetFrigateHost, putFrigateHostSchema} from '../services/frigate.proxy/frigate.schema';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import { Context } from '..';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
|
||||
@ -8,7 +8,7 @@ import { useClipboard } from '@mantine/hooks';
|
||||
import { configureMonacoYaml } from "monaco-yaml";
|
||||
import Editor, { DiffEditor, useMonaco, loader, Monaco } from '@monaco-editor/react'
|
||||
import * as monaco from "monaco-editor";
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
|
||||
|
||||
@ -79,25 +79,13 @@ const HostConfigPage = () => {
|
||||
return (
|
||||
<Flex direction='column' h='100%' w='100%' justify='stretch'>
|
||||
<Flex w='100%' justify='center' wrap='nowrap'>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mx-1"
|
||||
onClick={handleCopyConfig}
|
||||
>
|
||||
<Button size="sm" onClick={handleCopyConfig}>
|
||||
Copy Config
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mx-1"
|
||||
onClick={(_) => onHandleSaveConfig("restart")}
|
||||
>
|
||||
<Button ml='1rem' size="sm" onClick={(_) => onHandleSaveConfig("restart")}>
|
||||
Save & Restart
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
className="mx-1"
|
||||
onClick={(_) => onHandleSaveConfig("saveonly")}
|
||||
>
|
||||
<Button ml='1rem' size="sm" onClick={(_) => onHandleSaveConfig("saveonly")}>
|
||||
Save Only
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Context } from '..';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import Player from '../widgets/Player';
|
||||
import { Flex } from '@mantine/core';
|
||||
import { Button, Flex, Text } from '@mantine/core';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { recordingsPageQuery } from './RecordingsPage';
|
||||
|
||||
const LiveCameraPage = observer(() => {
|
||||
const navigate = useNavigate()
|
||||
let { id: cameraId } = useParams<'id'>()
|
||||
if (!cameraId) throw Error('Camera id does not exist')
|
||||
|
||||
@ -30,8 +34,21 @@ const LiveCameraPage = observer(() => {
|
||||
|
||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
const handleOpenRecordings = () => {
|
||||
if (camera.frigateHost) {
|
||||
const url = `${routesPath.RECORDINGS_PATH}?${recordingsPageQuery.hostId}=${camera.frigateHost.id}&${recordingsPageQuery.cameraId}=${camera.id}`
|
||||
navigate(url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%' justify='center'>
|
||||
<Flex w='100%' h='100%' justify='center' align='center' direction='column'>
|
||||
<Flex w='100%' justify='center' align='baseline' mb='1rem'>
|
||||
<Text mr='1rem'>{strings.camera}: {camera.name} {camera.frigateHost ? `/ ${camera.frigateHost.name}` : ''}</Text>
|
||||
{!camera.frigateHost ? <></> :
|
||||
<Button onClick={handleOpenRecordings}>{strings.recordings}</Button>
|
||||
}
|
||||
</Flex>
|
||||
<Player camera={camera} />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { Flex, Grid, Group } from '@mantine/core';
|
||||
import HeadSearch from '../shared/components/HeadSearch';
|
||||
import ViewSelector, { SelectorViewState } from '../shared/components/ViewSelector';
|
||||
import HeadSearch from '../shared/components/inputs/HeadSearch';
|
||||
import ViewSelector, { SelectorViewState } from '../shared/components/TableGridViewSelector';
|
||||
import { useContext, useState, useEffect } from 'react';
|
||||
import { getCookie, setCookie } from 'cookies-next';
|
||||
import { Context } from '..';
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import CameraCard from '../shared/components/CameraCard';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { dimensions } from '../shared/dimensions/dimensions';
|
||||
import CameraCard from '../widgets/CameraCard';
|
||||
|
||||
const MainPage = () => {
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
@ -40,8 +40,8 @@ const MainPage = () => {
|
||||
if (isError) return <RetryErrorPage onRetry={refetch} />
|
||||
|
||||
const cards = () => {
|
||||
return cameras.filter(cam => cam.frigateHost?.host.includes('5000')).slice(0, 25).map(camera => (
|
||||
// return cameras.map(camera => (
|
||||
// return cameras.filter(cam => cam.frigateHost?.host.includes('5000')).slice(0, 25).map(camera => (
|
||||
return cameras.map(camera => (
|
||||
<CameraCard
|
||||
key={camera.id}
|
||||
camera={camera}
|
||||
@ -60,7 +60,7 @@ const MainPage = () => {
|
||||
{/* <ViewSelector state={viewState} onChange={handleToggleState} /> */}
|
||||
</Flex>
|
||||
<Flex justify='center' h='100%' direction='column' w='100%' >
|
||||
<Grid mt='sm' justify="center" mb='sm' align='stretch' mr='0.5rem'>
|
||||
<Grid mt='sm' justify="center" mb='sm' align='stretch'>
|
||||
{cards()}
|
||||
</Grid>
|
||||
</Flex>
|
||||
|
||||
36
src/pages/PlayRecordPage.tsx
Normal file
36
src/pages/PlayRecordPage.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Flex } from '@mantine/core';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import VideoPlayer from '../shared/components/players/VideoPlayer';
|
||||
import { proxyURL } from '../shared/env.const';
|
||||
import { proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import NotFound from './404';
|
||||
import { Context } from '..';
|
||||
|
||||
export const playRecordPageQuery = {
|
||||
link: 'link',
|
||||
// hostName: 'hostName',
|
||||
}
|
||||
|
||||
const PlayRecordPage = () => {
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
useEffect(() => {
|
||||
sideBarsStore.rightVisible = false
|
||||
sideBarsStore.setLeftChildren(null)
|
||||
sideBarsStore.setRightChildren(null)
|
||||
}, [])
|
||||
|
||||
const location = useLocation()
|
||||
const queryParams = new URLSearchParams(location.search)
|
||||
const paramLink = queryParams.get(playRecordPageQuery.link)
|
||||
// const paramHostName = queryParams.get(playRecordPageQuery.hostName);
|
||||
|
||||
if (!paramLink) return (<NotFound />)
|
||||
return (
|
||||
<Flex w='100%' h='100%' justify='center' align='center' direction='column'>
|
||||
<VideoPlayer videoUrl={paramLink} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayRecordPage;
|
||||
@ -13,7 +13,7 @@ import { dateToQueryString, parseQueryDateToDate } from '../shared/utils/dateUti
|
||||
import SelecteDayList from '../widgets/SelecteDayList';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
|
||||
|
||||
export const recordingsPageQuery = {
|
||||
@ -54,28 +54,31 @@ const RecordingsPage = observer(() => {
|
||||
recStore.selectedRange = [parsedStartDay, parsedEndDay]
|
||||
}
|
||||
setFirstRender(true)
|
||||
return () => sideBarsStore.setRightChildren(null)
|
||||
return () => {
|
||||
sideBarsStore.setRightChildren(null)
|
||||
sideBarsStore.rightVisible = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setHostId(recStore.selectedHost?.id || '')
|
||||
if (recStore.selectedHost) {
|
||||
queryParams.set(recordingsPageQuery.hostId, recStore.selectedHost.id)
|
||||
setHostId(recStore.filteredHost?.id || '')
|
||||
if (recStore.filteredHost) {
|
||||
queryParams.set(recordingsPageQuery.hostId, recStore.filteredHost.id)
|
||||
} else {
|
||||
queryParams.delete(recordingsPageQuery.hostId)
|
||||
}
|
||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||
}, [recStore.selectedHost])
|
||||
}, [recStore.filteredHost])
|
||||
|
||||
useEffect(() => {
|
||||
setCameraId(recStore.selectedCamera?.id || '')
|
||||
if (recStore.selectedCamera) {
|
||||
queryParams.set(recordingsPageQuery.cameraId, recStore.selectedCamera?.id)
|
||||
setCameraId(recStore.filteredCamera?.id || '')
|
||||
if (recStore.filteredCamera) {
|
||||
queryParams.set(recordingsPageQuery.cameraId, recStore.filteredCamera?.id)
|
||||
} else {
|
||||
queryParams.delete(recordingsPageQuery.cameraId)
|
||||
}
|
||||
navigate({ pathname: location.pathname, search: queryParams.toString() });
|
||||
}, [recStore.selectedCamera])
|
||||
}, [recStore.filteredCamera])
|
||||
|
||||
useEffect(() => {
|
||||
setPeriod(recStore.selectedRange)
|
||||
|
||||
@ -5,10 +5,10 @@ import {
|
||||
useQueryClient,
|
||||
} from '@tanstack/react-query'
|
||||
import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.api';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from './RetryErrorPage';
|
||||
import { Button, Flex, Space } from '@mantine/core';
|
||||
import { FloatingLabelInput } from '../shared/components/FloatingLabelInput';
|
||||
import { FloatingLabelInput } from '../shared/components/inputs/FloatingLabelInput';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { dimensions } from '../shared/dimensions/dimensions';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
@ -3,13 +3,23 @@ import { observer } from 'mobx-react-lite';
|
||||
import { Button, Flex, Grid, Group, Indicator, Paper, Skeleton } from '@mantine/core';
|
||||
import RetryError from '../shared/components/RetryError';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import HeadSearch from '../shared/components/HeadSearch';
|
||||
import ViewSelector from '../shared/components/ViewSelector';
|
||||
import HeadSearch from '../shared/components/inputs/HeadSearch';
|
||||
import ViewSelector from '../shared/components/TableGridViewSelector';
|
||||
import { useIntersection } from '@mantine/hooks';
|
||||
import TestItem from './TestItem';
|
||||
import { Context } from '..';
|
||||
|
||||
const Test = observer(() => {
|
||||
const Test = () => {
|
||||
const [value, setValue] = useState<[Date | null, Date | null]>([null, null])
|
||||
const { sideBarsStore } = useContext(Context)
|
||||
sideBarsStore.rightVisible = true
|
||||
|
||||
useEffect(() => {
|
||||
sideBarsStore.rightVisible = true
|
||||
return () => {
|
||||
sideBarsStore.rightVisible = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('value', value)
|
||||
@ -54,6 +64,6 @@ const Test = observer(() => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
export default Test;
|
||||
export default observer(Test);
|
||||
@ -9,6 +9,7 @@ export const routesPath = {
|
||||
HOST_STORAGE_PATH: '/hosts/:id/storage',
|
||||
ACCESS_PATH: '/access',
|
||||
LIVE_PATH: '/cameras/:id/',
|
||||
PLAYER_PATH: '/player',
|
||||
THANKS_PATH: '/thanks',
|
||||
USER_DETAILED_PATH: '/user',
|
||||
RETRY_ERROR_PATH: '/retry_error',
|
||||
|
||||
@ -13,6 +13,7 @@ import HostStoragePage from "../pages/HostStoragePage";
|
||||
import LiveCameraPage from "../pages/LiveCameraPage";
|
||||
import RecordingsPage from "../pages/RecordingsPage";
|
||||
import AccessSettings from "../pages/AccessSettings";
|
||||
import PlayRecordPage from "../pages/PlayRecordPage";
|
||||
|
||||
interface IRoute {
|
||||
path: string,
|
||||
@ -56,6 +57,10 @@ export const routes: IRoute[] = [
|
||||
path: routesPath.LIVE_PATH,
|
||||
component: <LiveCameraPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.PLAYER_PATH,
|
||||
component: <PlayRecordPage />,
|
||||
},
|
||||
{
|
||||
path: routesPath.MAIN_PATH,
|
||||
component: <MainPage />,
|
||||
|
||||
@ -115,8 +115,9 @@ export const proxyApi = {
|
||||
recordingURL: (hostName: string, cameraName: string, timezone: string, day: string, hour: string) => {// day:2024-02-23 hour:19
|
||||
const parts = day.split('-')
|
||||
const date = `${parts[0]}-${parts[1]}/${parts[2]}/${hour}`
|
||||
return `${proxyURL.protocol}//${proxyURL.host}/proxy/${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8` // todo add Date/Time
|
||||
return `${proxyURL.protocol}//${proxyURL.host}/proxy/${hostName}/vod/${date}/${cameraName}/${timezone}/master.m3u8`
|
||||
},
|
||||
// linkURL: (hostName: string, link: string) => `${proxyURL.protocol}//${proxyURL.host}/proxy/${hostName}${link}`, TODO delete
|
||||
}
|
||||
|
||||
export const mapCamerasFromConfig = (config: FrigateConfig): string[] => {
|
||||
|
||||
@ -20,6 +20,7 @@ export const getFrigateHostSchema = z.object({
|
||||
name: z.string(),
|
||||
host: z.string(),
|
||||
enabled: z.boolean(),
|
||||
state: z.boolean().nullable().optional()
|
||||
});
|
||||
|
||||
export const getFrigateHostWConfigSchema = z.object({
|
||||
|
||||
@ -5,6 +5,7 @@ import CogwheelLoader from './loaders/CogwheelLoader';
|
||||
import RetryError from './RetryError';
|
||||
import { TransferList, Text, TransferListData, TransferListProps, TransferListItem, Button, Flex } from '@mantine/core';
|
||||
import { OneSelectItem } from './filters.aps/OneSelectFilter';
|
||||
import { strings } from '../strings/strings';
|
||||
|
||||
interface CamerasTransferListProps {
|
||||
roleId: string
|
||||
@ -48,7 +49,7 @@ const CamerasTransferList = ({
|
||||
|
||||
if (isPending) return <CogwheelLoader />
|
||||
if (isError || !cameras) return <RetryError onRetry={refetch} />
|
||||
if (cameras.length < 1) return <Text>Empty cameras </Text>
|
||||
if (cameras.length < 1) return <Text> {strings.camersDoesNotExist}</Text>
|
||||
|
||||
|
||||
const handleSave = () => {
|
||||
@ -63,8 +64,8 @@ const CamerasTransferList = ({
|
||||
return (
|
||||
<>
|
||||
<Flex w='100%' justify='center'>
|
||||
<Button mt='1rem' w='10%' miw='6rem' mr='1rem' onClick={handleDiscard}>Discard</Button>
|
||||
<Button mt='1rem' w='10%' miw='5rem' onClick={handleSave}>Save</Button>
|
||||
<Button mt='1rem' w='10%' miw='6rem' mr='1rem' onClick={handleDiscard}>{strings.discard}</Button>
|
||||
<Button mt='1rem' w='10%' miw='5rem' onClick={handleSave}>{strings.save}</Button>
|
||||
</Flex>
|
||||
<TransferList
|
||||
transferAllMatchingFilter
|
||||
@ -72,9 +73,9 @@ const CamerasTransferList = ({
|
||||
mt='1rem'
|
||||
value={lists}
|
||||
onChange={handleChange}
|
||||
searchPlaceholder="Search..."
|
||||
nothingFound="Nothing here"
|
||||
titles={['Not allowed', 'Allowed']}
|
||||
searchPlaceholder={strings.search}
|
||||
nothingFound={strings.nothingHere}
|
||||
titles={[strings.notAllowed, strings.allowed]}
|
||||
breakpoint="sm"
|
||||
/>
|
||||
</>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Carousel } from '@mantine/carousel';
|
||||
import { AspectRatio, Image, createStyles, Text } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
|
||||
import ImageWithPlaceHolder from './images/ImageWithPlaceHolder';
|
||||
|
||||
interface CardCarouselProps {
|
||||
image: string
|
||||
@ -1,13 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Text, TextProps } from '@mantine/core'
|
||||
import { strings } from '../strings/strings';
|
||||
|
||||
const Currency = (props: TextProps) => {
|
||||
return (
|
||||
<Text pl='0.2rem' fz="md" fw={500} {...props} >
|
||||
{strings.currency}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default Currency;
|
||||
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import { DeliveryMethods, DeliveryMethod } from '../stores/orders.store';
|
||||
import { Radio, Group } from '@mantine/core';
|
||||
import { strings } from '../strings/strings';
|
||||
|
||||
interface DeliveryMethodRadioProps {
|
||||
deliveryAvailable: boolean
|
||||
onChange(value: string): void
|
||||
deliveryMethod?: DeliveryMethod
|
||||
error?: string
|
||||
}
|
||||
|
||||
const DeliveryMethodRadio = ( {deliveryAvailable, deliveryMethod, onChange, error}: DeliveryMethodRadioProps ) => {
|
||||
|
||||
return (
|
||||
<Radio.Group
|
||||
size='lg'
|
||||
name="deliveryMethod"
|
||||
label={strings.delivery}
|
||||
description={strings.selectDeliveryMethod}
|
||||
withAsterisk
|
||||
onChange={onChange}
|
||||
value={deliveryMethod}
|
||||
error={error}
|
||||
>
|
||||
<Group mt="lg">
|
||||
{deliveryAvailable ?
|
||||
<Radio value={DeliveryMethods.Enum.delivery} label={strings.courierDelivery} />
|
||||
: <></>
|
||||
}
|
||||
<Radio value={DeliveryMethods.Enum.pickup} label={strings.pickUpByMyself} />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryMethodRadio;
|
||||
@ -1,43 +0,0 @@
|
||||
import { Radio, Group } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { strings } from '../strings/strings';
|
||||
import { it } from 'node:test';
|
||||
import { DeliveryPoint } from '../stores/user.store';
|
||||
|
||||
interface DeliveryPointRadioProps {
|
||||
data: DeliveryItem[],
|
||||
currentPoint?: DeliveryPoint,
|
||||
onChange(pointId: string): void,
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface DeliveryItem {
|
||||
id: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
const DeliveryPointRadio = ( {data, currentPoint, onChange, error}:DeliveryPointRadioProps ) => {
|
||||
|
||||
const radios = data.map( item => (
|
||||
<Radio key={item.id} value={item.id} label={item.name} />
|
||||
))
|
||||
|
||||
return (
|
||||
<Radio.Group
|
||||
size='lg'
|
||||
name="deliveryPoints"
|
||||
label={strings.deliveryPoint}
|
||||
description={strings.selectYourDeliveryAddress}
|
||||
withAsterisk
|
||||
onChange={onChange}
|
||||
defaultValue={currentPoint?.id}
|
||||
error={error}
|
||||
>
|
||||
<Group mt="lg">
|
||||
{radios}
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryPointRadio;
|
||||
@ -1,134 +0,0 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Context } from '../..';
|
||||
import CardCarousel from './grid.aps/CardCarousel';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Carousel, Embla, useAnimationOffsetEffect } from '@mantine/carousel';
|
||||
import { Modal, createStyles, getStylesRef, rem, Text, Box, Flex, Grid, Divider, Center } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { dimensions } from '../dimensions/dimensions';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import KomponentLoader from './loaders/CogwheelLoader';
|
||||
import ProductParameter from './ProductParameter';
|
||||
import { productString } from '../strings/product.strings';
|
||||
import { IconArrowBadgeLeft, IconArrowBadgeRight } from '@tabler/icons-react';
|
||||
import { strings } from '../strings/strings';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
modal: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
carousel: {
|
||||
flex: 1,
|
||||
'&:hover': {
|
||||
[`& .${getStylesRef('carouselControls')}`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
carouselControls: {
|
||||
ref: getStylesRef('carouselControls'),
|
||||
transition: 'opacity 150ms ease',
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
carouselIndicator: {
|
||||
width: rem(4),
|
||||
height: rem(4),
|
||||
transition: 'width 250ms ease',
|
||||
|
||||
'&[data-active]': {
|
||||
width: rem(16),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const FullProductModal = observer(() => {
|
||||
const { classes } = useStyles();
|
||||
const { modalStore } = useContext(Context)
|
||||
const { productDetailed, isProductDetailedOpened, closeProductDetailed } = modalStore
|
||||
|
||||
const isMobile = useMediaQuery(dimensions.mobileSize)
|
||||
|
||||
const TRANSITION_DURATION = 100
|
||||
const [embla, setEmbla] = useState<Embla | null>(null)
|
||||
useAnimationOffsetEffect(embla, TRANSITION_DURATION)
|
||||
|
||||
|
||||
const handleClose = () => {
|
||||
closeProductDetailed()
|
||||
}
|
||||
|
||||
const slides = productDetailed.data && productDetailed.data.image.length > 0
|
||||
?
|
||||
productDetailed.data.image.map((image) => (
|
||||
<CardCarousel
|
||||
key={uuidv4()}
|
||||
image={image}
|
||||
ratio={1}
|
||||
height="100%"
|
||||
/>))
|
||||
: null
|
||||
|
||||
const properties = productDetailed.data?.properties?.map( property => (
|
||||
<ProductParameter key={property.id} paramName={property.name} paramValue={property.value} />
|
||||
))
|
||||
|
||||
return (
|
||||
<Modal
|
||||
// size='auto'
|
||||
size='45%'
|
||||
opened={isProductDetailedOpened}
|
||||
onClose={handleClose}
|
||||
withCloseButton={true}
|
||||
centered
|
||||
fullScreen={isMobile}
|
||||
className={classes.modal}
|
||||
transitionProps={{ duration: TRANSITION_DURATION }}
|
||||
>
|
||||
{
|
||||
productDetailed.isLoading ?
|
||||
<KomponentLoader />
|
||||
:
|
||||
<Center>
|
||||
<Flex w="100%" direction='column' align='center'>
|
||||
{/* <Flex h='40rem' w='30rem' direction='column' align='center'> */}
|
||||
{/* <> */}
|
||||
<Carousel
|
||||
w='60%' // change image size
|
||||
getEmblaApi={setEmbla}
|
||||
withIndicators
|
||||
loop
|
||||
previousControlIcon={<IconArrowBadgeLeft />}
|
||||
nextControlIcon={<IconArrowBadgeRight />}
|
||||
controlSize={40}
|
||||
classNames={{
|
||||
root: classes.carousel,
|
||||
controls: classes.carouselControls,
|
||||
indicator: classes.carouselIndicator,
|
||||
}}
|
||||
>
|
||||
{slides}
|
||||
</Carousel>
|
||||
<Grid mt="1rem">
|
||||
{/* Base product parameters */}
|
||||
<ProductParameter paramName={productString.name} paramValue={productDetailed.data?.name} />
|
||||
<ProductParameter paramName={productString.number} paramValue={productDetailed.data?.number} />
|
||||
<ProductParameter paramName={productString.oem} paramValue={productDetailed.data?.oem} />
|
||||
<ProductParameter paramName={productString.stock} paramValue={productDetailed.data?.stock} />
|
||||
<ProductParameter paramName={productString.discount} paramValue={productDetailed.data?.discount? strings.true : strings.false} />
|
||||
<Divider w='100%' size="sm" />
|
||||
{/* Product properties */}
|
||||
{properties}
|
||||
{/* {productDetailed.data?.properties? JSON.stringify(productDetailed.data?.properties) : null} */}
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Center>
|
||||
// {/* </> */}
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
})
|
||||
|
||||
export default FullProductModal;
|
||||
@ -1,36 +0,0 @@
|
||||
import { Flex, Stepper } from '@mantine/core';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { strings } from '../strings/strings';
|
||||
import { Context } from '../..';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { PaymentMethod, PaymentMethods } from '../stores/orders.store';
|
||||
|
||||
const OrderStepper = observer(() => {
|
||||
|
||||
const { sideBarsStore, cartStore } = useContext(Context)
|
||||
|
||||
const { confirmedStage, paymentMethod } = cartStore
|
||||
|
||||
const stage = confirmedStage ? confirmedStage.stage + 1 : 0
|
||||
|
||||
const pb = '5rem'
|
||||
return (
|
||||
<Flex direction='column' justify='center' h='100%'>
|
||||
<Stepper
|
||||
h='20rem'
|
||||
active={stage}
|
||||
orientation="vertical"
|
||||
allowNextStepsSelect={false}
|
||||
>
|
||||
<Stepper.Step pb={pb} label={strings.cart} description={strings.confirmOrder} />
|
||||
<Stepper.Step pb={pb} label={strings.orderParams} description={strings.chooseParams} />
|
||||
{paymentMethod.data === PaymentMethods.Enum.Online ?
|
||||
<Stepper.Step pb={pb} label={strings.payment} description={strings.inputPaymentValues} />
|
||||
: <></>
|
||||
}
|
||||
</Stepper>
|
||||
</Flex>
|
||||
)
|
||||
})
|
||||
|
||||
export default OrderStepper;
|
||||
@ -1,73 +0,0 @@
|
||||
import { Button, Divider, Flex, Text } from '@mantine/core';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import PriceText from './PriceText';
|
||||
import Currency from './Currency';
|
||||
import { strings } from '../strings/strings';
|
||||
import { Context } from '../..';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const OrderTotals = () => {
|
||||
const navigate = useNavigate()
|
||||
const { cartStore } = useContext(Context)
|
||||
const { isLoading, products, totalWeight, totalSum,
|
||||
currentStage, confirmStage, confirmedStage, CartStages } = cartStore
|
||||
|
||||
const sizeBetween = '0.5rem'
|
||||
|
||||
const handleConfirm = () => {
|
||||
const maxIndex = cartStore.CartStages.length - 1
|
||||
const validate = confirmStage(currentStage)
|
||||
if (currentStage.stage === maxIndex) {
|
||||
console.log("currentStage.stage === maxIndex")
|
||||
return
|
||||
if (validate) {
|
||||
// todo send to server
|
||||
// navigate to main
|
||||
}
|
||||
}
|
||||
if (currentStage.stage < maxIndex) {
|
||||
const nextStageIndex = currentStage.stage + 1
|
||||
console.log("navigate")
|
||||
if (validate) {
|
||||
navigate(cartStore.CartStages[nextStageIndex].path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const backButton = () => {
|
||||
if (currentStage.stage === 0) {
|
||||
return <></>
|
||||
}
|
||||
return <Button onClick={() => navigate(-1)}>{strings.back}</Button>
|
||||
}
|
||||
|
||||
const okButton = () => {
|
||||
const lastStageIndex = CartStages.length - 1
|
||||
if (confirmedStage?.stage === CartStages[lastStageIndex].stage) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<Button onClick={handleConfirm} mb={sizeBetween}>{strings.confirm}</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex direction='column' h='100%' justify='center' align='center' gap='0.2rem'>
|
||||
<Text weight={700}>{strings.summary}</Text>
|
||||
<Divider pb={sizeBetween} w='100%' />
|
||||
<Text >{strings.positions}</Text>
|
||||
<Text>{products.length}</Text>
|
||||
<Divider pb={sizeBetween} w='100%' />
|
||||
<Text>{strings.weight}</Text>
|
||||
<Text >{totalWeight}</Text>
|
||||
<Divider pb={sizeBetween} w='100%' />
|
||||
<Text tt="uppercase">{strings.total}</Text>
|
||||
<Flex pb={sizeBetween}><PriceText value={totalSum} /><Currency /></Flex>
|
||||
<Divider pb={sizeBetween} w='100%' />
|
||||
{okButton()}
|
||||
{backButton()}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderTotals;
|
||||
@ -1,34 +0,0 @@
|
||||
import { Radio, Group } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { strings } from '../strings/strings';
|
||||
import { PaymentMethod, PaymentMethods } from '../stores/orders.store';
|
||||
|
||||
interface PaymentMehodRadioProps {
|
||||
onChange(value: PaymentMethod): void
|
||||
currentValue?: PaymentMethod
|
||||
error?: string
|
||||
}
|
||||
|
||||
const PaymentMehodRadio = ( {onChange, currentValue, error}: PaymentMehodRadioProps) => {
|
||||
|
||||
return (
|
||||
<Radio.Group
|
||||
size='lg'
|
||||
name="paymentMethod"
|
||||
label={strings.paymentMethod}
|
||||
description={strings.selectPaymentMethod}
|
||||
withAsterisk
|
||||
onChange={onChange}
|
||||
value={currentValue}
|
||||
error={error}
|
||||
>
|
||||
<Group mt="lg">
|
||||
<Radio value={PaymentMethods.Enum.Cash} label={strings.cashToCourier} />
|
||||
<Radio value={PaymentMethods.Enum.BankTransfer} label={strings.bankTransfer} />
|
||||
<Radio value={PaymentMethods.Enum.Online} label={strings.onlineByCard} />
|
||||
</Group>
|
||||
</Radio.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentMehodRadio;
|
||||
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Text, TextProps, createStyles } from '@mantine/core';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
price: {
|
||||
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}))
|
||||
|
||||
interface PriceTextProps extends TextProps {
|
||||
value: number,
|
||||
}
|
||||
|
||||
const PriceText = (props: PriceTextProps, ) => {
|
||||
const { classes } = useStyles()
|
||||
return (
|
||||
<Text {...props} className={classes.price}>
|
||||
{Intl.NumberFormat().format(props.value)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default PriceText;
|
||||
@ -1,27 +0,0 @@
|
||||
import { Divider, Flex, Grid, Text } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
interface ProductParameterProps {
|
||||
paramName?: string | number
|
||||
paramValue?: string | number | string[]
|
||||
}
|
||||
|
||||
const ProductParameter = ({paramName, paramValue}: ProductParameterProps) => {
|
||||
|
||||
// if (!paramValue) return null
|
||||
|
||||
const pl='1rem', pr='0.2rem', pt='0.1rem', pb='0.2rem'
|
||||
return (
|
||||
<>
|
||||
<Grid.Col pl={pl} pr={pr} pt={pt} pb={pb} span={6}>
|
||||
<Text fw={500}>{paramName}</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col pl={pl} pr={pr} pt={pt} pb={pb} span={6}>
|
||||
<Text>{paramValue}</Text>
|
||||
</Grid.Col>
|
||||
<Divider w='100%' size="xs" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductParameter;
|
||||
@ -1,30 +0,0 @@
|
||||
import { NavLink } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
interface TreeLinkProps {
|
||||
id: string
|
||||
label: string
|
||||
selected: boolean,
|
||||
opened?: boolean,
|
||||
onClick(selectedId: string): void,
|
||||
children?: (JSX.Element | undefined)[]
|
||||
}
|
||||
const TreeLink = ({id, label, selected, opened, onClick, children}: TreeLinkProps) => {
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
opened={opened}
|
||||
pt='2px'
|
||||
pb='2px'
|
||||
active={selected}
|
||||
label={label}
|
||||
childrenOffset={18}
|
||||
onClick={() => onClick(id)}
|
||||
defaultOpened={false}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default TreeLink;
|
||||
@ -4,7 +4,7 @@ import { useAuth } from 'react-oidc-context';
|
||||
import { strings } from '../strings/strings';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { dimensions } from '../dimensions/dimensions';
|
||||
import ColorSchemeToggle from './ColorSchemeToggle';
|
||||
import ColorSchemeToggle from './buttons/ColorSchemeToggle';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { keycloakConfig } from '../..';
|
||||
|
||||
|
||||
@ -12,16 +12,19 @@ import { strings } from '../../strings/strings';
|
||||
import { RecordSummary } from '../../../types/record';
|
||||
|
||||
interface CameraAccordionProps {
|
||||
camera: GetCameraWHostWConfig,
|
||||
host: GetFrigateHost
|
||||
// camera: GetCameraWHostWConfig,
|
||||
// host: GetFrigateHost
|
||||
}
|
||||
|
||||
const CameraAccordion = ({
|
||||
camera,
|
||||
host
|
||||
// camera,
|
||||
// host
|
||||
}: CameraAccordionProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const camera = recStore.openedCamera || recStore.filteredCamera
|
||||
const host = recStore.filteredHost
|
||||
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, camera?.id],
|
||||
queryFn: () => {
|
||||
@ -33,20 +36,6 @@ const CameraAccordion = ({
|
||||
}
|
||||
})
|
||||
|
||||
const [openedDay, setOpenedDay] = useState<string | null>()
|
||||
|
||||
useEffect(() => {
|
||||
if (openedDay) {
|
||||
recStore.recordToPlay.cameraName = camera.name
|
||||
const hostName = mapHostToHostname(host)
|
||||
recStore.recordToPlay.hostName = hostName
|
||||
}
|
||||
}, [openedDay])
|
||||
|
||||
const handleClick = (value: string | null) => {
|
||||
setOpenedDay(value)
|
||||
}
|
||||
|
||||
if (isPending) return <Center><Loader /></Center>
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
|
||||
@ -83,7 +72,7 @@ const CameraAccordion = ({
|
||||
console.log('CameraAccordion rendered')
|
||||
|
||||
return (
|
||||
<Accordion variant='separated' radius="md" w='100%' onChange={handleClick}>
|
||||
<Accordion variant='separated' radius="md" w='100%'>
|
||||
{days()}
|
||||
</Accordion>
|
||||
)
|
||||
|
||||
@ -1,14 +1,19 @@
|
||||
import { Accordion, Center, Group, Text } from '@mantine/core';
|
||||
import { Accordion, Center, Flex, Group, NavLink, Text, UnstyledButton } from '@mantine/core';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { RecordSummary } from '../../../types/record';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import PlayControl from './PlayControl';
|
||||
import { proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import PlayControl from '../buttons/PlayControl';
|
||||
import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { Context } from '../../..';
|
||||
import VideoPlayer from '../players/VideoPlayer';
|
||||
import { getResolvedTimeZone } from '../../utils/dateUtil';
|
||||
import DayEventsAccordion from './DayEventsAccordion';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import AccordionControlButton from '../buttons/AccordionControlButton';
|
||||
import { IconExternalLink, IconShare } from '@tabler/icons-react';
|
||||
import { routesPath } from '../../../router/routes.path';
|
||||
import AccordionShareButton from '../buttons/AccordionShareButton';
|
||||
|
||||
interface RecordingAccordionProps {
|
||||
recordSummary?: RecordSummary
|
||||
@ -18,55 +23,65 @@ const DayAccordion = ({
|
||||
recordSummary
|
||||
}: RecordingAccordionProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
|
||||
const [playedValue, setVideoPlayerState] = useState<string>()
|
||||
const [openedValue, setOpenedValue] = useState<string>()
|
||||
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||
const [link, setLink] = useState<string>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const camera = recStore.openedCamera || recStore.filteredCamera
|
||||
|
||||
const createRecordURL = (recordId: string): string | undefined => {
|
||||
const record = {
|
||||
hostName: recStore.filteredHost ? mapHostToHostname(recStore.filteredHost) : '',
|
||||
cameraName: camera?.name,
|
||||
day: recordSummary?.day,
|
||||
hour: recordId,
|
||||
timezone: getResolvedTimeZone().replace('/', ','),
|
||||
}
|
||||
const parsed = recStore.getFullRecordForPlay(record)
|
||||
if (parsed.success) {
|
||||
return proxyApi.recordingURL(
|
||||
parsed.data.hostName,
|
||||
parsed.data.cameraName,
|
||||
parsed.data.timezone,
|
||||
parsed.data.day,
|
||||
parsed.data.hour
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (openVideoPlayer) {
|
||||
console.log('openVideoPlayer', openVideoPlayer)
|
||||
if (openVideoPlayer) {
|
||||
recStore.recordToPlay.day = recordSummary?.day
|
||||
recStore.recordToPlay.hour = openVideoPlayer
|
||||
recStore.recordToPlay.timezone = getResolvedTimeZone().replace('/', ',')
|
||||
const parsed = recStore.getFullRecordForPlay(recStore.recordToPlay)
|
||||
console.log('recordingsStore.playedRecord: ', recStore.recordToPlay)
|
||||
if (parsed.success) {
|
||||
const url = proxyApi.recordingURL(
|
||||
parsed.data.hostName,
|
||||
parsed.data.cameraName,
|
||||
parsed.data.timezone,
|
||||
parsed.data.day,
|
||||
parsed.data.hour
|
||||
)
|
||||
console.log('GET URL: ', url)
|
||||
setPlayerUrl(url)
|
||||
}
|
||||
if (playedValue) {
|
||||
const url = createRecordURL(playedValue)
|
||||
if (url) {
|
||||
console.log('GET URL: ', url)
|
||||
setPlayerUrl(url)
|
||||
}
|
||||
} else {
|
||||
setPlayerUrl(undefined)
|
||||
}
|
||||
}, [openVideoPlayer])
|
||||
}, [playedValue])
|
||||
|
||||
if (!recordSummary ) return <Text>Not have record at that day</Text>
|
||||
if (recordSummary.hours.length < 1) return <Text>Not have record at that day</Text>
|
||||
if (!recordSummary || recordSummary.hours.length < 1) return <Text>Not have record at that day</Text>
|
||||
|
||||
const handleOpenPlayer = (hour: string) => {
|
||||
if (openVideoPlayer !== hour) {
|
||||
setOpenedValue(hour)
|
||||
setOpenVideoPlayer(hour)
|
||||
} else if (openedValue === hour && openVideoPlayer === hour) {
|
||||
setOpenVideoPlayer(undefined)
|
||||
const handleOpenPlayer = (value: string) => {
|
||||
if (playedValue !== value) {
|
||||
setOpenedValue(value)
|
||||
setVideoPlayerState(value)
|
||||
} else if (openedValue === value && playedValue === value) {
|
||||
setVideoPlayerState(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (value: string) => {
|
||||
const handleOpenItem = (value: string) => {
|
||||
if (openedValue === value) {
|
||||
setOpenedValue(undefined)
|
||||
} else {
|
||||
setOpenedValue(value)
|
||||
}
|
||||
setOpenVideoPlayer(undefined)
|
||||
setVideoPlayerState(undefined)
|
||||
}
|
||||
|
||||
console.log('DayAccordion rendered')
|
||||
@ -82,25 +97,42 @@ const DayAccordion = ({
|
||||
</Group>
|
||||
)
|
||||
|
||||
const hanleOpenNewLink = (recordId: string) => {
|
||||
const link = createRecordURL(recordId)
|
||||
if (link) {
|
||||
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}`
|
||||
navigate(url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
key={recordSummary.day}
|
||||
variant='separated'
|
||||
radius="md" w='100%'
|
||||
value={openedValue}
|
||||
onChange={handleClick}
|
||||
onChange={handleOpenItem}
|
||||
>
|
||||
{recordSummary.hours.map(hour => (
|
||||
<Accordion.Item key={hour.hour + 'Item'} value={hour.hour}>
|
||||
<Accordion.Control key={hour.hour + 'Control'}>
|
||||
<PlayControl
|
||||
label={hourLabel(hour.hour, hour.events)}
|
||||
value={hour.hour}
|
||||
openVideoPlayer={openVideoPlayer}
|
||||
onClick={handleOpenPlayer} />
|
||||
<Flex justify='space-between'>
|
||||
{hourLabel(hour.hour, hour.events)}
|
||||
<Group>
|
||||
<AccordionShareButton recordUrl={createRecordURL(hour.hour)}/>
|
||||
<AccordionControlButton onClick={() => hanleOpenNewLink(hour.hour)}>
|
||||
<IconExternalLink />
|
||||
</AccordionControlButton>
|
||||
<PlayControl
|
||||
value={hour.hour}
|
||||
playedValue={playedValue}
|
||||
onClick={handleOpenPlayer} />
|
||||
</Group>
|
||||
</Flex>
|
||||
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel key={hour.hour + 'Panel'}>
|
||||
{openVideoPlayer === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
{playedValue === hour.hour && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
{hour.events > 0 ?
|
||||
<DayEventsAccordion day={recordSummary.day} hour={hour.hour} qty={hour.events} />
|
||||
:
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
import { Accordion, Center, Group, Loader, Text } from '@mantine/core';
|
||||
import { Accordion, Center, Flex, Group, Loader, Text } from '@mantine/core';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Context } from '../../..';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateQueryKeys, mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { getEventsQuerySchema } from '../../../services/frigate.proxy/frigate.schema';
|
||||
import PlayControl from './PlayControl';
|
||||
import PlayControl from '../buttons/PlayControl';
|
||||
import VideoPlayer from '../players/VideoPlayer';
|
||||
import { getDurationFromTimestamps, getUnixTime, unixTimeToDate } from '../../utils/dateUtil';
|
||||
import RetryError from '../RetryError';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { EventFrigate } from '../../../types/event';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import { routesPath } from '../../../router/routes.path';
|
||||
import AccordionControlButton from '../buttons/AccordionControlButton';
|
||||
import AccordionShareButton from '../buttons/AccordionShareButton';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* @param day frigate format, e.g day: 2024-02-23
|
||||
@ -37,13 +42,15 @@ const EventsAccordion = observer(({
|
||||
// TODO labels, score
|
||||
}: EventsAccordionProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const [openVideoPlayer, setOpenVideoPlayer] = useState<string>()
|
||||
const [openedValue, setOpenedValue] = useState<string>()
|
||||
const [playedValue, setPlayedValue] = useState<string>()
|
||||
const [openedItem, setOpenedItem] = useState<string>()
|
||||
const [playerUrl, setPlayerUrl] = useState<string>()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const inHost = recStore.selectedHost
|
||||
const inCamera = recStore.selectedCamera
|
||||
const inHost = recStore.filteredHost
|
||||
const inCamera = recStore.openedCamera || recStore.filteredCamera
|
||||
const isRequiredParams = inHost && inCamera
|
||||
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getEvents, inHost, inCamera, day, hour],
|
||||
queryFn: () => {
|
||||
@ -76,40 +83,47 @@ const EventsAccordion = observer(({
|
||||
}
|
||||
})
|
||||
|
||||
const createEventUrl = (eventId: string) => {
|
||||
if (inHost)
|
||||
return proxyApi.eventURL(mapHostToHostname(inHost), eventId)
|
||||
return undefined
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (openVideoPlayer) {
|
||||
console.log('openVideoPlayer', openVideoPlayer)
|
||||
if (openVideoPlayer && inHost) {
|
||||
const url = proxyApi.eventURL(mapHostToHostname(inHost), openVideoPlayer)
|
||||
if (playedValue) {
|
||||
// console.log('openVideoPlayer', playedValue)
|
||||
if (playedValue && inHost) {
|
||||
const url = createEventUrl(playedValue)
|
||||
console.log('GET EVENT URL: ', url)
|
||||
setPlayerUrl(url)
|
||||
}
|
||||
} else {
|
||||
setPlayerUrl(undefined)
|
||||
}
|
||||
}, [openVideoPlayer])
|
||||
}, [playedValue])
|
||||
|
||||
if (isPending) return <Center><Loader /></Center>
|
||||
if (isError) return <RetryError onRetry={refetch} />
|
||||
if (!data || data.length < 1) return <Center><Text>Not have events at that period</Text></Center>
|
||||
|
||||
const handleOpenPlayer = (eventId: string) => {
|
||||
// console.log(`openVideoPlayer day:${recordSummary.day} hour:${hour}`)
|
||||
if (openVideoPlayer !== eventId) {
|
||||
setOpenedValue(eventId)
|
||||
setOpenVideoPlayer(eventId)
|
||||
} else if (openedValue === eventId && openVideoPlayer === eventId) {
|
||||
setOpenVideoPlayer(undefined)
|
||||
const handleOpenPlayer = (openedValue: string) => {
|
||||
// console.log(`openVideoPlayer day:${day} hour:${hour}, opened value: ${openedValue}`)
|
||||
// console.log(`opened value: ${openedValue}, eventId: ${playedValue}`)
|
||||
if (openedValue !== playedValue) {
|
||||
setOpenedItem(openedValue)
|
||||
setPlayedValue(openedValue)
|
||||
} else if (openedValue === playedValue && playedValue === playedValue) {
|
||||
setPlayedValue(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (value: string) => {
|
||||
if (openedValue === value) {
|
||||
setOpenedValue(undefined)
|
||||
const handleOpenItem = (value: string) => {
|
||||
if (playedValue === value) {
|
||||
setOpenedItem(undefined)
|
||||
} else {
|
||||
setOpenedValue(value)
|
||||
setOpenedItem(value)
|
||||
}
|
||||
setOpenVideoPlayer(undefined)
|
||||
setPlayedValue(undefined)
|
||||
}
|
||||
|
||||
const eventLabel = (event: EventFrigate) => {
|
||||
@ -126,24 +140,40 @@ const EventsAccordion = observer(({
|
||||
)
|
||||
}
|
||||
|
||||
const hanleOpenNewLink = (recordId: string) => {
|
||||
const link = createEventUrl(recordId)
|
||||
if (link) {
|
||||
const url = `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(link)}`
|
||||
navigate(url)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
variant='separated'
|
||||
radius="md" w='100%'
|
||||
value={openedValue}
|
||||
onChange={handleClick}
|
||||
value={openedItem}
|
||||
onChange={handleOpenItem}
|
||||
>
|
||||
{data.map(event => (
|
||||
<Accordion.Item key={event.id + 'Item'} value={event.id}>
|
||||
<Accordion.Control key={event.id + 'Control'}>
|
||||
<PlayControl
|
||||
label={eventLabel(event)}
|
||||
value={event.id}
|
||||
openVideoPlayer={openVideoPlayer}
|
||||
onClick={handleOpenPlayer} />
|
||||
<Flex justify='space-between'>
|
||||
{eventLabel(event)}
|
||||
<Group>
|
||||
<AccordionShareButton recordUrl={createEventUrl(event.id)} />
|
||||
<AccordionControlButton onClick={() => hanleOpenNewLink(event.id)}>
|
||||
<IconExternalLink />
|
||||
</AccordionControlButton>
|
||||
<PlayControl
|
||||
value={event.id}
|
||||
playedValue={playedValue}
|
||||
onClick={handleOpenPlayer} />
|
||||
</Group>
|
||||
</Flex>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel key={event.id + 'Panel'}>
|
||||
{openVideoPlayer === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
{playedValue === event.id && playerUrl ? <VideoPlayer videoUrl={playerUrl} /> : <></>}
|
||||
<Group mt='1rem'>
|
||||
<Text>{strings.camera}: {event.camera}</Text>
|
||||
<Text>{strings.player.object}: {event.label}</Text>
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Flex, Group, Text, createStyles } from '@mantine/core';
|
||||
import { IconPlayerPlay, IconPlayerPlayFilled, IconPlayerStop, IconPlayerStopFilled } from '@tabler/icons-react';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
group: {
|
||||
backgroundColor: theme.colors.blue[7],
|
||||
borderRadius: '1rem',
|
||||
paddingLeft: '0.5rem',
|
||||
paddingRight: '0.3rem',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.fn.darken(theme.colors.blue[7], 0.2),
|
||||
},
|
||||
},
|
||||
text: {
|
||||
color: theme.white,
|
||||
fontWeight: 'normal'
|
||||
},
|
||||
iconStop: {
|
||||
color: theme.colors.red[5]
|
||||
},
|
||||
iconPlay: {
|
||||
color: theme.colors.green[5]
|
||||
}
|
||||
}))
|
||||
|
||||
interface PlayControlProps {
|
||||
label: string | JSX.Element,
|
||||
value: string,
|
||||
openVideoPlayer?: string,
|
||||
onClick?: (value: string) => void
|
||||
}
|
||||
|
||||
const PlayControl = ({
|
||||
label,
|
||||
value,
|
||||
openVideoPlayer,
|
||||
onClick
|
||||
}: PlayControlProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleClick = (value: string) => {
|
||||
if (onClick) onClick(value)
|
||||
}
|
||||
return (
|
||||
<Flex justify='space-between'>
|
||||
{label}
|
||||
<Group className={classes.group}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}}
|
||||
>
|
||||
<Text className={classes.text}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}}>
|
||||
{openVideoPlayer === value ? strings.player.stopVideo : strings.player.startVideo}
|
||||
</Text>
|
||||
{openVideoPlayer === value ?
|
||||
<IconPlayerStopFilled
|
||||
className={classes.iconStop}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}} />
|
||||
:
|
||||
<IconPlayerPlayFilled
|
||||
className={classes.iconPlay}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
handleClick(value)
|
||||
}} />
|
||||
|
||||
}
|
||||
</Group>
|
||||
</Flex>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayControl;
|
||||
46
src/shared/components/buttons/AccordionControlButton.tsx
Normal file
46
src/shared/components/buttons/AccordionControlButton.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Button, ButtonProps, Group, UnstyledButton, createStyles } from '@mantine/core';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.colors.blue[7],
|
||||
borderRadius: '1rem',
|
||||
paddingLeft: '1rem',
|
||||
paddingRight: '1rem',
|
||||
paddingTop: '0.4rem',
|
||||
paddingBottom: '0.4rem',
|
||||
color: theme.white,
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.fn.darken(theme.colors.blue[7], 0.2),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
interface AccordionControlButtonProps extends ButtonProps {
|
||||
children?: React.ReactNode,
|
||||
onClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
const AccordionControlButton = ({
|
||||
children,
|
||||
onClick,
|
||||
...rest
|
||||
} : AccordionControlButtonProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
event.stopPropagation()
|
||||
if (onClick) onClick(event)
|
||||
}
|
||||
return (
|
||||
<Group
|
||||
onClick={handleClick}
|
||||
className={classes.button} {...rest}>
|
||||
{children}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionControlButton;
|
||||
39
src/shared/components/buttons/AccordionShareButton.tsx
Normal file
39
src/shared/components/buttons/AccordionShareButton.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { IconShare } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import AccordionControlButton from './AccordionControlButton';
|
||||
import { routesPath } from '../../../router/routes.path';
|
||||
|
||||
interface AccordionShareButtonProps {
|
||||
recordUrl?: string
|
||||
}
|
||||
|
||||
const AccordionShareButton = ({
|
||||
recordUrl
|
||||
}: AccordionShareButtonProps) => {
|
||||
const canShare = Boolean(navigator.share);
|
||||
const clipboard = useClipboard()
|
||||
const url = recordUrl ? `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(recordUrl)}` : ''
|
||||
|
||||
const handleShare = async () => {
|
||||
if (canShare && url) {
|
||||
try {
|
||||
await navigator.share({ url });
|
||||
console.log('Content shared successfully');
|
||||
} catch (err) {
|
||||
console.error('Error sharing content: ', err);
|
||||
}
|
||||
} else {
|
||||
clipboard.copy(url)
|
||||
console.log('URL copied to clipboard')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AccordionControlButton>
|
||||
<IconShare onClick={handleShare} />
|
||||
</AccordionControlButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionShareButton;
|
||||
@ -1,6 +1,6 @@
|
||||
import { Tooltip, CloseButton, CloseButtonProps } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { strings } from '../strings/strings';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
|
||||
interface CloseWithTooltipProps {
|
||||
52
src/shared/components/buttons/PlayControl.tsx
Normal file
52
src/shared/components/buttons/PlayControl.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Flex, Group, Text, createStyles } from '@mantine/core';
|
||||
import { IconPlayerPlay, IconPlayerPlayFilled, IconPlayerStop, IconPlayerStopFilled } from '@tabler/icons-react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import AccordionControlButton from './AccordionControlButton';
|
||||
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
iconStop: {
|
||||
color: theme.colors.red[5]
|
||||
},
|
||||
iconPlay: {
|
||||
color: theme.colors.green[5]
|
||||
}
|
||||
}))
|
||||
|
||||
interface PlayControlProps {
|
||||
value: string,
|
||||
playedValue?: string,
|
||||
onClick?: (value: string) => void
|
||||
}
|
||||
|
||||
const PlayControl = ({
|
||||
value,
|
||||
playedValue,
|
||||
onClick
|
||||
}: PlayControlProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleClick = (value: string) => {
|
||||
if (onClick) onClick(value)
|
||||
}
|
||||
return (
|
||||
<AccordionControlButton
|
||||
onClick={() => { handleClick(value) }}
|
||||
>
|
||||
<Flex align='center'>
|
||||
{playedValue === value ? strings.player.stopVideo : strings.player.startVideo}
|
||||
{playedValue === value ?
|
||||
<IconPlayerStopFilled
|
||||
className={classes.iconStop} />
|
||||
:
|
||||
<IconPlayerPlayFilled
|
||||
className={classes.iconPlay} />
|
||||
|
||||
}
|
||||
</Flex>
|
||||
</AccordionControlButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlayControl;
|
||||
@ -27,7 +27,7 @@ const CameraSelectFilter = ({
|
||||
if (!data) return
|
||||
if (recStore.cameraIdParam) {
|
||||
console.log('change camera by param')
|
||||
recStore.selectedCamera = data.cameras.find( camera => camera.id === recStore.cameraIdParam)
|
||||
recStore.filteredCamera = data.cameras.find( camera => camera.id === recStore.cameraIdParam)
|
||||
recStore.cameraIdParam = undefined
|
||||
}
|
||||
}, [isSuccess])
|
||||
@ -41,10 +41,10 @@ const CameraSelectFilter = ({
|
||||
const handleSelect = (value: string) => {
|
||||
const camera = data.cameras.find(camera => camera.id === value)
|
||||
if (!camera) {
|
||||
recStore.selectedCamera = undefined
|
||||
recStore.filteredCamera = undefined
|
||||
return
|
||||
}
|
||||
recStore.selectedCamera = camera
|
||||
recStore.filteredCamera = camera
|
||||
}
|
||||
|
||||
console.log('CameraSelectFilter rendered')
|
||||
@ -55,8 +55,8 @@ const CameraSelectFilter = ({
|
||||
id='frigate-cameras'
|
||||
label={strings.selectCamera}
|
||||
spaceBetween='1rem'
|
||||
value={recStore.selectedCamera?.id || ''}
|
||||
defaultValue={recStore.selectedCamera?.id || ''}
|
||||
value={recStore.filteredCamera?.id || ''}
|
||||
defaultValue={recStore.filteredCamera?.id || ''}
|
||||
data={camerasItems}
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
|
||||
@ -3,7 +3,7 @@ import { observer } from 'mobx-react-lite';
|
||||
import React, { useContext } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { Box, Flex, Indicator, Text } from '@mantine/core';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { Context } from '../../..';
|
||||
|
||||
interface DateRangeSelectFilterProps {
|
||||
|
||||
@ -20,7 +20,7 @@ const HostSelectFilter = () => {
|
||||
useEffect(() => {
|
||||
if (!hosts) return
|
||||
if (recStore.hostIdParam) {
|
||||
recStore.selectedHost = hosts.find(host => host.id === recStore.hostIdParam)
|
||||
recStore.filteredHost = hosts.find(host => host.id === recStore.hostIdParam)
|
||||
recStore.hostIdParam = undefined
|
||||
}
|
||||
}, [isSuccess])
|
||||
@ -37,14 +37,14 @@ const HostSelectFilter = () => {
|
||||
const handleSelect = (value: string) => {
|
||||
const host = hosts?.find(host => host.id === value)
|
||||
if (!host) {
|
||||
recStore.selectedHost = undefined
|
||||
recStore.selectedCamera = undefined
|
||||
recStore.filteredHost = undefined
|
||||
recStore.filteredCamera = undefined
|
||||
return
|
||||
}
|
||||
if (recStore.selectedHost?.id !== host.id) {
|
||||
recStore.selectedCamera = undefined
|
||||
if (recStore.filteredHost?.id !== host.id) {
|
||||
recStore.filteredCamera = undefined
|
||||
}
|
||||
recStore.selectedHost = host
|
||||
recStore.filteredHost = host
|
||||
}
|
||||
|
||||
return (
|
||||
@ -52,8 +52,8 @@ const HostSelectFilter = () => {
|
||||
id='frigate-hosts'
|
||||
label={strings.selectHost}
|
||||
spaceBetween='1rem'
|
||||
value={recStore.selectedHost?.id || ''}
|
||||
defaultValue={recStore.selectedHost?.id || ''}
|
||||
value={recStore.filteredHost?.id || ''}
|
||||
defaultValue={recStore.filteredHost?.id || ''}
|
||||
data={hostItems}
|
||||
onChange={handleSelect}
|
||||
/>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SystemProp, SpacingValue, Box, Flex, CloseButton, MultiSelect, SelectItem, MultiSelectProps, Text, Tooltip } from '@mantine/core';
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
|
||||
interface MultiSelectFilterProps {
|
||||
id: string
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { SelectItem, SystemProp, SpacingValue, SelectProps, Box, Flex, CloseButton, Text, Select } from '@mantine/core';
|
||||
import React, { CSSProperties } from 'react';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { SystemProp, SpacingValue, SliderProps, Box, RangeSlider, RangeSliderProps, Text, Flex, CloseButton } from '@mantine/core';
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface SliderFilterProps {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Box, CloseButton, Flex, Slider, SliderProps, SpacingValue, SystemProp, Text } from '@mantine/core';
|
||||
import React, { CSSProperties, useState, } from 'react';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface SliderFilterProps {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { SystemProp, SpacingValue, Flex, Switch, Text, CloseButton, Group, Box } from '@mantine/core';
|
||||
import React, { CSSProperties, ChangeEvent } from 'react';
|
||||
import { boolean } from 'zod';
|
||||
import CloseWithTooltip from '../CloseWithTooltip';
|
||||
import CloseWithTooltip from '../buttons/CloseWithTooltip';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface SwitchFilterProps {
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
import { useCounter } from '@mantine/hooks';
|
||||
import RowCounter from '../table.aps/RowCounter';
|
||||
import { Button, createStyles } from '@mantine/core';
|
||||
import { productString } from '../../strings/product.strings';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
counter: {
|
||||
height: Button.defaultProps?.h?.toString() || '36px'
|
||||
},
|
||||
}))
|
||||
interface BuyCounterToggleProps {
|
||||
counter?: number
|
||||
setValue?(value: number): void
|
||||
}
|
||||
|
||||
const BuyCounterToggle = ({ counter, setValue }: BuyCounterToggleProps) => {
|
||||
const { classes } = useStyles();
|
||||
// const [count, handlers] = useCounter(counter, { min: 0 })
|
||||
|
||||
const handleSetCount = (value: number) => {
|
||||
if (setValue) setValue(value)
|
||||
// else handlers.set(value)
|
||||
}
|
||||
|
||||
const handleBuyClick = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation()
|
||||
handleSetCount(1)
|
||||
}
|
||||
|
||||
// useEffect(() => {
|
||||
// console.log("render BuyCounterToggle")
|
||||
// })
|
||||
|
||||
if (counter && counter > 0) {
|
||||
return (
|
||||
<div className={classes.counter}>
|
||||
<RowCounter key={uuidv4()} counter={counter} setValue={handleSetCount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Button w="100%" onClick={handleBuyClick}
|
||||
> {productString.buy}</Button >
|
||||
);
|
||||
};
|
||||
|
||||
export default BuyCounterToggle;
|
||||
|
||||
@ -1,184 +0,0 @@
|
||||
import { Card, Group, createStyles, getStylesRef, rem, Text, Container, Badge, ColSpan, Grid, Flex } from '@mantine/core';
|
||||
import React, { useContext } from 'react';
|
||||
import { Carousel } from '@mantine/carousel';
|
||||
import { GridAdapter } from './ProductGrid';
|
||||
import CardCarousel from './CardCarousel';
|
||||
import BuyCounterToggle from './BuyCounterToggle';
|
||||
import Currency from '../Currency';
|
||||
import { Context } from '../../..';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import PriceText from '../PriceText';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
|
||||
mainCard: {
|
||||
borderWidth: '1px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '12rem',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.fn.darken(theme.colors.cyan[9], 0.5) : theme.colors.cyan[1],
|
||||
},
|
||||
},
|
||||
|
||||
bottomGroup: {
|
||||
marginTop: 'auto',
|
||||
},
|
||||
|
||||
priceContainer: {
|
||||
marginTop: '0.8rem',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: "0.5rem",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
price: {
|
||||
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||
fontSize: "lg",
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
productNameGroup: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
gap: "0.5rem",
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
productName: {
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
carousel: {
|
||||
'&:hover': {
|
||||
[`& .${getStylesRef('carouselControls')}`]: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
carouselControls: {
|
||||
ref: getStylesRef('carouselControls'),
|
||||
transition: 'opacity 150ms ease',
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
carouselIndicator: {
|
||||
width: rem(4),
|
||||
height: rem(4),
|
||||
transition: 'width 250ms ease',
|
||||
|
||||
'&[data-active]': {
|
||||
width: rem(16),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
interface GridCardProps {
|
||||
span?: ColSpan,
|
||||
item: GridAdapter
|
||||
}
|
||||
|
||||
const GridCard = observer(({ span, item }: GridCardProps) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const { modalStore, cartStore } = useContext(Context)
|
||||
const { openFullImage, openProductDetailed } = modalStore
|
||||
|
||||
const prodId = item.id
|
||||
const prodName: string = item.name
|
||||
const prodImages: string[] = item.image
|
||||
const prodPrice: number = item.cost
|
||||
const prodDiscount: boolean = item.discount
|
||||
const prodQty: number = item.qty
|
||||
//todo replace to real price
|
||||
const min = 1.0;
|
||||
const max = 1.99;
|
||||
const randomNumber = Math.random() * (max - min) + min;
|
||||
const prodDiscountPrice: number = Number((item.cost * randomNumber).toFixed(2))
|
||||
const prodDiscountPercent: number = parseFloat(((prodPrice / prodDiscountPrice - 1) * 100).toFixed(0))
|
||||
|
||||
const slides = prodImages.length > 0
|
||||
?
|
||||
prodImages.map((image) => (
|
||||
<CardCarousel
|
||||
key={uuidv4()}
|
||||
image={image}
|
||||
ratio={1}
|
||||
height={190}
|
||||
onClick={() => openFullImage(item.image)}
|
||||
/>))
|
||||
:
|
||||
<CardCarousel
|
||||
key={uuidv4()}
|
||||
image=''
|
||||
ratio={100 / 100}
|
||||
height={190} />
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid.Col span={span} >
|
||||
<Card radius="lg" padding='4px' withBorder className={classes.mainCard}>
|
||||
<Card.Section>
|
||||
<Carousel
|
||||
withIndicators
|
||||
loop
|
||||
classNames={{
|
||||
root: classes.carousel,
|
||||
controls: classes.carouselControls,
|
||||
indicator: classes.carouselIndicator,
|
||||
}}
|
||||
>
|
||||
{slides}
|
||||
</Carousel>
|
||||
</Card.Section>
|
||||
|
||||
<Container
|
||||
onClick={() => openProductDetailed(prodId)}
|
||||
className={classes.priceContainer}
|
||||
fluid>
|
||||
{prodDiscount ?
|
||||
<Group pl="0.3rem" style={{ display: 'flex', flexWrap: 'nowrap', gap: '0', alignItems: 'center' }}>
|
||||
<PriceText td='line-through' fs='oblique' fz='sm' value={prodDiscountPrice} />
|
||||
<Currency fz='sm' />
|
||||
<Badge style={{ alignSelf: 'center' }} color='red' size='lg' p="0">{prodDiscountPercent}%</Badge>
|
||||
</Group>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Group style={{ display: 'flex', flexWrap: 'nowrap', gap: '0', alignItems: 'flex-end' }}>
|
||||
<PriceText value={prodPrice} />
|
||||
<Currency />
|
||||
</Group>
|
||||
</Container>
|
||||
|
||||
<Group
|
||||
onClick={() => openProductDetailed(prodId)}
|
||||
mt='0.5rem'
|
||||
className={classes.productNameGroup}
|
||||
position="apart">
|
||||
<Text fz='sm' align="center" fw='500' className={classes.productName}>
|
||||
{prodName}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group
|
||||
className={classes.bottomGroup}>
|
||||
<Flex w='100%' justify='center' mt="0.5rem">
|
||||
<BuyCounterToggle counter={item.qty} setValue={(value) => {
|
||||
console.log("value", value)
|
||||
cartStore.setToCart(item, value)
|
||||
}} />
|
||||
</Flex>
|
||||
</Group>
|
||||
</Card>
|
||||
</Grid.Col >
|
||||
</>
|
||||
);
|
||||
})
|
||||
|
||||
export default GridCard;
|
||||
@ -1,36 +0,0 @@
|
||||
import { Card, Grid, rem } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import GridCard from './GridCard';
|
||||
import BuyCounterToggle from './BuyCounterToggle';
|
||||
import RowCounter from '../table.aps/RowCounter';
|
||||
|
||||
export type GridAdapter = {
|
||||
id: string,
|
||||
number: number,
|
||||
manufactory: string,
|
||||
oem: string,
|
||||
stock: number,
|
||||
receipt_date: string,
|
||||
name: string,
|
||||
cost: number,
|
||||
image: string[],
|
||||
discount: boolean,
|
||||
qty: number
|
||||
}
|
||||
|
||||
interface ProductGridProps {
|
||||
gridData: GridAdapter[]
|
||||
}
|
||||
|
||||
const ProductGrid = ({ gridData }: ProductGridProps) => {
|
||||
const span = "content"
|
||||
const grids = gridData.map( item => ( <GridCard key={item.id} span={span} item={item} />))
|
||||
|
||||
return (
|
||||
<Grid pt="1rem" justify='center' align='stretch'>
|
||||
{grids}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductGrid;
|
||||
@ -1,23 +0,0 @@
|
||||
import { Grid, Flex, Text } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { strings } from '../../strings/strings';
|
||||
|
||||
interface ProfileRowProps {
|
||||
name?: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
const ProfileRow = ({ name, value }: ProfileRowProps) => {
|
||||
return (
|
||||
<>
|
||||
<Grid.Col span={6}>
|
||||
<Text fz='md'>{name}</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Text align='center' fz='md'>{value}</Text>
|
||||
</Grid.Col>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileRow;
|
||||
@ -1,10 +1,10 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { CameraConfig } from "../../types/frigateConfig";
|
||||
import { CameraConfig } from "../../../types/frigateConfig";
|
||||
import { Flex, Text, Image } from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { frigateApi, proxyApi } from "../../services/frigate.proxy/frigate.api";
|
||||
import { frigateApi, proxyApi } from "../../../services/frigate.proxy/frigate.api";
|
||||
import { useIntersection } from "@mantine/hooks";
|
||||
import CogwheelLoader from "./loaders/CogwheelLoader";
|
||||
import CogwheelLoader from "../loaders/CogwheelLoader";
|
||||
|
||||
interface AutoUpdatedImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
||||
className?: string;
|
||||
@ -1,6 +1,6 @@
|
||||
import { TextInput, TextInputProps, createStyles } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import classes from './css/FloatingLabelInput.module.css';
|
||||
import classes from './FloatingLabelInput.module.css';
|
||||
|
||||
interface FloatingLabelInputProps extends TextInputProps {
|
||||
value?: string,
|
||||
@ -1,7 +1,7 @@
|
||||
import { Flex, TextInput } from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import ViewSelector from './ViewSelector';
|
||||
import ViewSelector from '../TableGridViewSelector';
|
||||
|
||||
interface HeadSearchProps {
|
||||
search?: string
|
||||
@ -1,6 +1,6 @@
|
||||
import { DEFAULT_THEME, Loader, LoadingOverlay } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import СogwheelSVG from './svg/CogwheelSVG';
|
||||
import СogwheelSVG from '../svg/CogwheelSVG';
|
||||
|
||||
const CenterLoader = () => {
|
||||
return <LoadingOverlay loader={СogwheelSVG} visible />;
|
||||
@ -2,7 +2,7 @@ import { Box, Burger, Button, Center, Collapse, Divider, Drawer, Flex, Group, Me
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconChevronDown } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { LinkItem } from '../../widgets/header/HeaderAction';
|
||||
import { LinkItem } from '../../../widgets/header/HeaderAction';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@ -3,29 +3,35 @@ import { IconEdit, IconGraph, IconMessageCircle, IconRotateClockwise, IconServer
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../../../router/routes.path';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
|
||||
import { GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
|
||||
|
||||
interface HostSettingsMenuProps {
|
||||
id: string
|
||||
host: GetFrigateHost
|
||||
}
|
||||
|
||||
const HostSettingsMenu = ({ id }: HostSettingsMenuProps) => {
|
||||
const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => {
|
||||
const navigate = useNavigate()
|
||||
const mutation = useMutation({
|
||||
mutationFn: (hostName: string) => proxyApi.getHostRestart(hostName)
|
||||
})
|
||||
|
||||
const handleConfig = () => {
|
||||
const url = routesPath.HOST_CONFIG_PATH.replace(':id', id)
|
||||
const url = routesPath.HOST_CONFIG_PATH.replace(':id', host.id)
|
||||
navigate(url)
|
||||
}
|
||||
const handleStorage = () => {
|
||||
const url = routesPath.HOST_STORAGE_PATH.replace(':id', id)
|
||||
const url = routesPath.HOST_STORAGE_PATH.replace(':id', host.id)
|
||||
navigate(url)
|
||||
}
|
||||
const handleSystem = () => {
|
||||
const url = routesPath.HOST_SYSTEM_PATH.replace(':id', id)
|
||||
const url = routesPath.HOST_SYSTEM_PATH.replace(':id', host.id)
|
||||
navigate(url)
|
||||
}
|
||||
|
||||
const handleRestart = () => {
|
||||
throw Error('Not yet implemented')
|
||||
mutation.mutate(mapHostToHostname(host))
|
||||
}
|
||||
return (
|
||||
<Menu shadow="md" width={200}>
|
||||
|
||||
@ -2,11 +2,11 @@ import { Carousel, Embla, useAnimationOffsetEffect } from '@mantine/carousel';
|
||||
import { Modal, createStyles, getStylesRef, rem } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import CardCarousel from './grid.aps/CardCarousel';
|
||||
import { Context } from '../..';
|
||||
import CardCarousel from '../CardCarousel';
|
||||
import { Context } from '../../..';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { dimensions } from '../dimensions/dimensions';
|
||||
import { dimensions } from '../../dimensions/dimensions';
|
||||
|
||||
// change to http://react-responsive-carousel.js.org/
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@ -1,9 +1,9 @@
|
||||
import { ActionIcon, CloseButton, Flex, Modal, NumberInput, TextInput, Tooltip, createStyles, } from '@mantine/core';
|
||||
import { getHotkeyHandler, useMediaQuery } from '@mantine/hooks';
|
||||
import React, { ReactEventHandler, useState, FocusEvent, useRef, Ref } from 'react';
|
||||
import { strings } from '../strings/strings';
|
||||
import { strings } from '../../strings/strings';
|
||||
import { IconAlertCircle, IconX } from '@tabler/icons-react';
|
||||
import { dimensions } from '../dimensions/dimensions';
|
||||
import { dimensions } from '../../dimensions/dimensions';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
rightSection: {
|
||||
@ -1,53 +1,74 @@
|
||||
// @ts-ignore we know this doesn't have types
|
||||
import JSMpeg from "@cycjimmy/jsmpeg-player";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Flex } from "@mantine/core";
|
||||
import { useViewportSize } from "@mantine/hooks";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { strings } from "../../strings/strings";
|
||||
|
||||
type JSMpegPlayerProps = {
|
||||
wsUrl: string;
|
||||
cameraHeight?: number,
|
||||
cameraWidth?: number,
|
||||
};
|
||||
|
||||
|
||||
const JSMpegPlayer = (
|
||||
{
|
||||
wsUrl,
|
||||
|
||||
cameraWidth = 1200,
|
||||
cameraHeight = 800,
|
||||
}: JSMpegPlayerProps
|
||||
) => {
|
||||
const videoRef = useRef<HTMLCanvasElement>(null);
|
||||
const playerRef = useRef<HTMLDivElement>(null);
|
||||
const [playerInitialized, setPlayerInitialized] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let player: any;
|
||||
const { height: maxHeight, width: maxWidth } = useViewportSize()
|
||||
|
||||
if (player && playerInitialized) {
|
||||
player.destroy()
|
||||
console.log('JSMpegPlayer destroyed player')
|
||||
}
|
||||
if (!playerInitialized && videoRef.current) {
|
||||
console.log('JSMpegPlayer creating player')
|
||||
player = new JSMpeg.Player(
|
||||
wsUrl,
|
||||
{ canvas: videoRef.current },
|
||||
{},
|
||||
{ protocols: [], audio: false }
|
||||
);
|
||||
setPlayerInitialized(true);
|
||||
}
|
||||
useEffect(() => {
|
||||
const video = new JSMpeg.VideoElement(
|
||||
playerRef.current,
|
||||
wsUrl,
|
||||
{},
|
||||
{protocols: [], audio: false, videoBufferSize: 1024*1024*4}
|
||||
);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
const canvas = video.els.canvas;
|
||||
if (!document.fullscreenElement && !(document as any).webkitFullscreenElement) { // Use bracket notation for webkit
|
||||
// Enter fullscreen
|
||||
if (canvas.requestFullscreen) {
|
||||
canvas.requestFullscreen();
|
||||
} else if ((canvas as any).webkitRequestFullScreen) { // Use bracket notation for webkit
|
||||
(canvas as any).webkitRequestFullScreen();
|
||||
} else if (canvas.mozRequestFullScreen) {
|
||||
canvas.mozRequestFullScreen();
|
||||
}
|
||||
} else {
|
||||
// Exit fullscreen
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if ((document as any).webkitExitFullscreen) { // Use bracket notation for webkit
|
||||
(document as any).webkitExitFullscreen();
|
||||
} else if ((document as any).mozCancelFullScreen) {
|
||||
(document as any).mozCancelFullScreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
video.els.canvas.addEventListener('dblclick',toggleFullscreen);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
console.log('JSMpegPlayer destroying player')
|
||||
player.destroy()
|
||||
console.log('JSMpegPlayer destroyed player')
|
||||
setPlayerInitialized(false)
|
||||
} catch (error) {
|
||||
setPlayerInitialized(true)
|
||||
console.error('JSMpegPlayer Error on unmount:', error);
|
||||
}
|
||||
video.destroy();
|
||||
video.els.canvas.removeEventListener('dblclick', toggleFullscreen);
|
||||
};
|
||||
}, [wsUrl]);
|
||||
|
||||
return <canvas key={wsUrl} ref={videoRef}></canvas>;
|
||||
return (
|
||||
<div
|
||||
ref={playerRef}
|
||||
key={wsUrl}
|
||||
title={strings.player.doubleClickToFullHint}
|
||||
style={{width:cameraWidth, height:cameraHeight, maxWidth:maxWidth, maxHeight: maxHeight-100, }} />
|
||||
)
|
||||
};
|
||||
|
||||
export default JSMpegPlayer
|
||||
@ -1,7 +1,7 @@
|
||||
import { ActionIcon, Badge, Box, Flex, Text, useMantineTheme } from '@mantine/core';
|
||||
import { useCounter, useDisclosure } from '@mantine/hooks';
|
||||
import { IconMinus, IconPlus, IconX } from '@tabler/icons-react';
|
||||
import InputModal from '../InputModal';
|
||||
import InputModal from '../modal.windows/InputModal';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { useEffect } from 'react';
|
||||
|
||||
|
||||
@ -1,129 +0,0 @@
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import RowCounter from './RowCounter';
|
||||
import { Badge, Center, Flex, Group, Text, createStyles } from '@mantine/core';
|
||||
import { TableAdapter } from '../../../widgets/ProductTable';
|
||||
import ImageWithPlaceHolder from '../ImageWithPlaceHolder';
|
||||
import Currency from '../Currency';
|
||||
import { Context } from '../../..';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import PriceText from '../PriceText';
|
||||
|
||||
interface TableRowProps {
|
||||
element: TableAdapter
|
||||
selected: string
|
||||
increase?(id: string): void
|
||||
decrease?(id: string): void
|
||||
setQty?(id: string, value: number): void
|
||||
onDelete?(id: string): void
|
||||
showDelete?: boolean
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
tableRow: {
|
||||
'&:hover': {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[9] : theme.colors.gray[1],
|
||||
},
|
||||
},
|
||||
rowSelected: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark'
|
||||
? theme.fn.darken(theme.colors.cyan[9], 0.5)
|
||||
: theme.colors.cyan[1],
|
||||
},
|
||||
|
||||
price: {
|
||||
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
|
||||
fontSize: "lg",
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
discountPrice: {
|
||||
fontSize: "sm",
|
||||
fontWeight: 500,
|
||||
textDecoration: "line-through",
|
||||
fontStyle: 'oblique',
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
const TableRow = observer(({ element, selected, increase, decrease, setQty, onDelete, showDelete }: TableRowProps) => {
|
||||
const { classes, cx } = useStyles()
|
||||
const mainRef: React.LegacyRef<HTMLTableRowElement> = useRef(null)
|
||||
|
||||
const { modalStore } = useContext(Context)
|
||||
const { openFullImage, openProductDetailed } = modalStore
|
||||
|
||||
//todo replace to real price
|
||||
const min = 1.0;
|
||||
const max = 1.99;
|
||||
const randomNumber = Math.random() * (max - min) + min;
|
||||
const prodDiscountPrice: number = Number((element.cost * randomNumber).toFixed(2))
|
||||
const prodDiscountPercent: number = parseFloat(((element.cost / prodDiscountPrice - 1) * 100).toFixed(0))
|
||||
|
||||
const handleSetQty = (value: number) => {
|
||||
if (setQty) setQty(element.id, value)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (onDelete) onDelete(element.id)
|
||||
}
|
||||
|
||||
return (
|
||||
<tr ref={mainRef}
|
||||
className={cx(
|
||||
{ [classes.rowSelected]: selected === element.id },
|
||||
classes.tableRow,
|
||||
)}
|
||||
>
|
||||
<td onClick={() => {openProductDetailed(element.id)}}><Text fz='sm' fw='500'>{element.name}</Text></td>
|
||||
<td>
|
||||
< Flex direction='column' wrap='nowrap' gap='0' align='center' >
|
||||
{element.discount ?
|
||||
<Flex wrap='nowrap' gap='0' align='center' >
|
||||
<Text fz='sm' className={classes.discountPrice}>
|
||||
{Intl.NumberFormat().format(prodDiscountPrice)}
|
||||
</Text>
|
||||
<Currency fz='sm' />
|
||||
<Badge style={{ alignSelf: 'center' }} color='red' size='md' p="0">{prodDiscountPercent}%</Badge>
|
||||
</Flex>
|
||||
:
|
||||
null
|
||||
}
|
||||
<Flex wrap='nowrap' gap='0' align='flex-end' >
|
||||
<PriceText value={element.cost} fz='sm' fw='500' />
|
||||
<Currency fz='sm' />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</td>
|
||||
<td style={{ backgroundSize: "cover" }}>
|
||||
<ImageWithPlaceHolder
|
||||
onClick={() => openFullImage(element.image)}
|
||||
mah="4rem"
|
||||
mih="2rem"
|
||||
height="3rem"
|
||||
src={element.image[0]} />
|
||||
</td>
|
||||
<td style={{paddingLeft: 5, paddingRight: 5}}>
|
||||
<Center>
|
||||
<Text fz='sm' fw='500'>
|
||||
{Intl.NumberFormat().format(element.stock)}
|
||||
</Text>
|
||||
</Center>
|
||||
</td>
|
||||
<td >
|
||||
<Center>
|
||||
<RowCounter
|
||||
counter={element.qty}
|
||||
setValue={handleSetQty}
|
||||
onDelete={handleDelete}
|
||||
showDelete={showDelete}
|
||||
/>
|
||||
</Center>
|
||||
</td>
|
||||
</tr >
|
||||
);
|
||||
})
|
||||
|
||||
|
||||
export default TableRow;
|
||||
@ -1,246 +0,0 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { Product } from "./product.store";
|
||||
import { sleep } from "../utils/async.sleep";
|
||||
import { TableAdapter } from "../../widgets/ProductTable";
|
||||
import { DeliveryMethod, DeliveryMethods, PaymentMethod, PaymentMethods } from "./orders.store";
|
||||
import { addItem, removeItemById } from "../utils/array.helper";
|
||||
import { strings } from "../strings/strings";
|
||||
import { Validated } from "../utils/validated";
|
||||
import { DeliveryPoint, DeliveryPointSchema } from "./user.store";
|
||||
|
||||
export interface CartProduct extends Product {
|
||||
qty: number,
|
||||
}
|
||||
|
||||
export interface OrderingStage {
|
||||
stage: number,
|
||||
path: string,
|
||||
}
|
||||
|
||||
// TODO delete
|
||||
export class CartStore {
|
||||
|
||||
|
||||
readonly cartStage1: OrderingStage = { stage: 0, path: 'CART_PATH' }
|
||||
readonly cartStage2: OrderingStage = { stage: 1, path: 'CART_METHOD_PATH' }
|
||||
readonly cartStage3: OrderingStage = { stage: 2, path: 'PAYMENT_PATH' }
|
||||
|
||||
private _CartStages: OrderingStage[] = [
|
||||
this.cartStage1,
|
||||
this.cartStage2
|
||||
]
|
||||
|
||||
public get CartStages() {
|
||||
return this._CartStages
|
||||
}
|
||||
|
||||
private _confirmedStage?: OrderingStage
|
||||
public get confirmedStage() {
|
||||
return this._confirmedStage;
|
||||
}
|
||||
private _currentStage: OrderingStage = this._CartStages[0]
|
||||
public get currentStage() {
|
||||
return this._currentStage;
|
||||
}
|
||||
|
||||
private _paymentMethod = new Validated<PaymentMethod>
|
||||
public get paymentMethod() {
|
||||
return this._paymentMethod;
|
||||
}
|
||||
|
||||
private _deliveryMethod = new Validated<DeliveryMethod>
|
||||
public get deliveryMethod() {
|
||||
return this._deliveryMethod;
|
||||
}
|
||||
|
||||
private _deliveryDate = new Validated<Date>
|
||||
public get deliveryDate() {
|
||||
return this._deliveryDate;
|
||||
}
|
||||
|
||||
private _deliveryPoint = new Validated<DeliveryPoint>
|
||||
public get deliveryPoint() {
|
||||
return this._deliveryPoint;
|
||||
}
|
||||
|
||||
private _products: CartProduct[] = []
|
||||
public get products() {
|
||||
return this._products;
|
||||
}
|
||||
|
||||
public get totalSum(): number {
|
||||
const totalSum = this._products.reduce((sum, productCart) => {
|
||||
const productCost = productCart.cost
|
||||
const productQty = productCart.qty
|
||||
return sum + productCost * productQty
|
||||
}, 0);
|
||||
return totalSum;
|
||||
}
|
||||
|
||||
public get totalWeight(): number {
|
||||
const totalWeight = this._products.reduce((sum, product) => {
|
||||
const weight = product.weight
|
||||
const qty = product.qty
|
||||
if (weight) return sum + weight * qty
|
||||
else return sum
|
||||
}, 0)
|
||||
return totalWeight
|
||||
}
|
||||
|
||||
_isLoading = false
|
||||
public get isLoading() {
|
||||
return this._isLoading;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
setPaymentMethod = (method: PaymentMethod) => {
|
||||
console.log("setPaymentMethod", method)
|
||||
if (method === PaymentMethods.Enum.Online) {
|
||||
this._CartStages = addItem(this.cartStage3, this._CartStages)
|
||||
} else {
|
||||
this._CartStages = this._CartStages.filter(item => item.stage !== this.cartStage3.stage)
|
||||
}
|
||||
this._paymentMethod = this._paymentMethod.set(method)
|
||||
}
|
||||
setDeliveryPoint = (point: DeliveryPoint) => {
|
||||
console.log("setDeliveryPoint", point)
|
||||
this._deliveryPoint = this.deliveryPoint.set(point)
|
||||
}
|
||||
setDeliveryDate = (date: Date) => {
|
||||
console.log("setDeliveryDate", date)
|
||||
this._deliveryDate = this.deliveryDate.set(date)
|
||||
}
|
||||
|
||||
setDeliveryMethod = (method: DeliveryMethod) => {
|
||||
console.log("setDeliveryMethod", method)
|
||||
|
||||
if (method == DeliveryMethods.Enum.pickup) {
|
||||
this._deliveryDate = this._deliveryDate.set(undefined)
|
||||
this._deliveryPoint = this._deliveryPoint.set(undefined)
|
||||
}
|
||||
this._deliveryMethod = this.deliveryMethod.set(method)
|
||||
}
|
||||
|
||||
isPreviosStageConfirmed = (currentStage: OrderingStage): boolean => {
|
||||
const previosStage = this._CartStages[currentStage.stage - 1]
|
||||
if (this.confirmedStage && this._confirmedStage!.stage >= previosStage.stage) return true
|
||||
return false
|
||||
}
|
||||
|
||||
confirmStage = (stage: OrderingStage) => {
|
||||
console.log("confirmStage", stage)
|
||||
if (stage.stage == this._confirmedStage?.stage) {
|
||||
console.log("same Stage")
|
||||
return true
|
||||
}
|
||||
switch (stage.stage) {
|
||||
case 0: {
|
||||
// return this._products.length > 0
|
||||
this._confirmedStage = stage
|
||||
return true
|
||||
}
|
||||
case 1: {
|
||||
this.validateStage1()
|
||||
const validateAll = !this._deliveryMethod.error && !this._paymentMethod.error
|
||||
&& !this._deliveryPoint.error && !this._deliveryDate.error
|
||||
if (validateAll) {
|
||||
this._confirmedStage = stage
|
||||
return true // todo send to server
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
setConfirmed = (stage?: OrderingStage) => {
|
||||
this._confirmedStage = stage
|
||||
}
|
||||
|
||||
setCurrentStage = (stage: OrderingStage) => {
|
||||
this._currentStage = stage
|
||||
}
|
||||
|
||||
private validateStage1() {
|
||||
const isPayment = PaymentMethods.safeParse(this._paymentMethod.data).success
|
||||
this._paymentMethod = this._paymentMethod.validate(isPayment, strings.errors.choosePaymentMethod)
|
||||
const isDelivery = DeliveryMethods.safeParse(this._deliveryMethod.data).success
|
||||
this._deliveryMethod = this._deliveryMethod.validate(isDelivery, strings.errors.chooseDeliveryMethod)
|
||||
if (this._deliveryMethod.data === DeliveryMethods.Enum.delivery) {
|
||||
const isDeliveryPoint = DeliveryPointSchema.safeParse(this._deliveryPoint.data).success
|
||||
this._deliveryPoint = this._deliveryPoint.validate(isDeliveryPoint, strings.errors.chooseDeliveryPoint)
|
||||
const isDeliveryDate = this._deliveryDate.data instanceof Date
|
||||
this._deliveryDate = this._deliveryDate.validate(isDeliveryDate, strings.errors.chooseDate)
|
||||
}
|
||||
}
|
||||
|
||||
updateCartFromServer = async () => {
|
||||
try {
|
||||
this._isLoading = true
|
||||
const res = await this.fetchCartFromServer()
|
||||
runInAction(() => {
|
||||
this._products = res
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
runInAction(() => {
|
||||
this._isLoading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setToCart = async (product: Product, productQty: number) => {
|
||||
if (product) {
|
||||
const currentValue = this._products.find(productCart => productCart.id === product.id)
|
||||
if (currentValue) {
|
||||
if (productQty === 0) this._products = this._products.filter(item => item !== currentValue)
|
||||
if (productQty > 0) this._products = this._products.map(item => {
|
||||
if (item.id === currentValue.id) return { ...item, qty: productQty }
|
||||
return item
|
||||
})
|
||||
} else if (productQty > 0) {
|
||||
this._products.push({ ...product, qty: productQty })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteFromCart = (id: string) => {
|
||||
this._products = removeItemById(id, this._products)
|
||||
}
|
||||
|
||||
sortCart = (reverse: boolean) => {
|
||||
this._products = this._products.reverse()
|
||||
}
|
||||
|
||||
private async sendCartToServer(card: CartProduct) {
|
||||
await sleep(100)
|
||||
}
|
||||
|
||||
private async deleteCartFromServer(card: CartProduct) {
|
||||
await sleep(100)
|
||||
}
|
||||
|
||||
private async fetchCartFromServer(): Promise<CartProduct[]> {
|
||||
await sleep(300)
|
||||
if (this._products) return this._products
|
||||
return []
|
||||
}
|
||||
|
||||
mapFromTable(tableItem: TableAdapter) {
|
||||
const cart: CartProduct = {
|
||||
...tableItem
|
||||
}
|
||||
return cart
|
||||
}
|
||||
|
||||
mapToTable(cartProduct: CartProduct): TableAdapter {
|
||||
return {
|
||||
...cartProduct
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import { OrderingStage } from "./cart.store"
|
||||
|
||||
export const validate = (stage: OrderingStage) => {
|
||||
console.log("confirmStage", stage)
|
||||
switch (stage.stage) {
|
||||
case 0: {
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
break
|
||||
}
|
||||
}
|
||||
return validate
|
||||
}
|
||||
|
||||
@ -1,55 +0,0 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx"
|
||||
import { sleep } from "../utils/async.sleep"
|
||||
|
||||
export type Category = {
|
||||
id: string,
|
||||
name: string,
|
||||
childs: string[],
|
||||
parent?: string,
|
||||
isAcive: boolean,
|
||||
}
|
||||
|
||||
export class CategoryStore {
|
||||
private _categories: Category[] = []
|
||||
public get categories(): Category[] {
|
||||
return this._categories
|
||||
}
|
||||
|
||||
private _selectedCategory: string = ""
|
||||
public get selectedCategory(): string {
|
||||
return this._selectedCategory
|
||||
}
|
||||
|
||||
private _isLoading = false
|
||||
public get isLoading() {
|
||||
return this._isLoading
|
||||
}
|
||||
|
||||
constructor () {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
|
||||
updateCategories = async () => {
|
||||
try {
|
||||
this._isLoading = true
|
||||
const res = await this.fetchCategoriesFromServer()
|
||||
runInAction( () => {
|
||||
this._categories = res
|
||||
this._isLoading = false
|
||||
})
|
||||
} catch {
|
||||
this._isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
selectCategory = (categoryId: string) => {
|
||||
if (this._selectedCategory === categoryId) this._selectedCategory = ''
|
||||
else this._selectedCategory = categoryId
|
||||
}
|
||||
|
||||
private async fetchCategoriesFromServer(): Promise<Category[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx"
|
||||
import { sleep } from "../../utils/async.sleep"
|
||||
import { ServerFilter } from "./filters.interface"
|
||||
import { ServerFilter } from "./toDel.filters.interface"
|
||||
import { addItem, removeFilter, removeItem } from "../../utils/array.helper"
|
||||
import { valueIsNotEmpty } from "../../utils/any.helper"
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx"
|
||||
import RootStore from "./root.store"
|
||||
import { Product } from "./product.store"
|
||||
import { Resource } from "../utils/resource"
|
||||
|
||||
export class ModalStore {
|
||||
@ -16,16 +15,6 @@ export class ModalStore {
|
||||
return this._fullImageData
|
||||
}
|
||||
|
||||
private _isProductDetailedOpened = false
|
||||
public get isProductDetailedOpened() {
|
||||
return this._isProductDetailedOpened
|
||||
}
|
||||
|
||||
private _productDetailed = new Resource<Product>
|
||||
public get productDetailed(): Resource<Product> {
|
||||
return this._productDetailed
|
||||
}
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
this.rootStore = rootStore
|
||||
makeAutoObservable(this)
|
||||
@ -41,29 +30,4 @@ export class ModalStore {
|
||||
this._isFullImageOpened = false
|
||||
}
|
||||
|
||||
closeProductDetailed = () => {
|
||||
this._productDetailed = { isLoading: false, data: undefined }
|
||||
this._isProductDetailedOpened = false
|
||||
}
|
||||
|
||||
openProductDetailed = async (id: string) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this._productDetailed.isLoading = true
|
||||
this._isProductDetailedOpened = true
|
||||
})
|
||||
const res = await this.rootStore.productStore.getProductDetailed(id) // todo make this one instance or aborted
|
||||
runInAction(() => {
|
||||
if (res) {
|
||||
this._productDetailed = { ...this._productDetailed, data: res }
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error) this._productDetailed.error = error
|
||||
} finally {
|
||||
this._productDetailed.isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
import { makeAutoObservable } from "mobx"
|
||||
import { Resource } from "../utils/resource"
|
||||
import { DeliveryPoint } from "./user.store"
|
||||
import { Product } from "./product.store"
|
||||
import { z } from 'zod'
|
||||
|
||||
export interface Order {
|
||||
id: string, // uuid
|
||||
number: string,
|
||||
date: string,
|
||||
name: string,
|
||||
sum: number,
|
||||
status: OrderStatus,
|
||||
paymentStatus?: PaymentStatus,
|
||||
paymentMethod?: PaymentMethod,
|
||||
deliveryMethod?: DeliveryMethod,
|
||||
deliveryPoint?: DeliveryPoint,
|
||||
deliveryDate?: Date,
|
||||
products?: Product[],
|
||||
}
|
||||
|
||||
export interface OrterProduct extends Product{
|
||||
qty: number
|
||||
}
|
||||
|
||||
export const PaymentMethods = z.enum([
|
||||
'Cash',
|
||||
'BankTransfer',
|
||||
'Online',
|
||||
])
|
||||
export type PaymentMethod = z.infer<typeof PaymentMethods>
|
||||
|
||||
|
||||
export const DeliveryMethods = z.enum(['pickup','delivery'])
|
||||
export type DeliveryMethod = z.infer<typeof DeliveryMethods>
|
||||
|
||||
export enum OrderStatus {
|
||||
Draft = 'draft',
|
||||
Сonfirmed = 'confirmed',
|
||||
Processed = 'processed',
|
||||
Delivering = 'delivering',
|
||||
Finished = 'finished',
|
||||
Deleted = 'deleted',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
Paid = 'paid',
|
||||
NotPaid = 'notpaid',
|
||||
}
|
||||
|
||||
export class OrdersStore {
|
||||
|
||||
private _orders = new Resource<Order[]>
|
||||
public get orders() {
|
||||
return this._orders
|
||||
}
|
||||
|
||||
private _isLoading = false
|
||||
public get isLoading() {
|
||||
return this._isLoading
|
||||
}
|
||||
|
||||
constructor () {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
updateOrders = async () => {
|
||||
this._isLoading = true
|
||||
try {
|
||||
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this._isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
async fetchOrdersFromServer () {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
import { makeAutoObservable, runInAction } from "mobx";
|
||||
import { TableAdapter } from "../../widgets/ProductTable";
|
||||
import { GridAdapter } from "../components/grid.aps/ProductGrid";
|
||||
import { sleep } from "../utils/async.sleep";
|
||||
import { CartProduct } from "./cart.store";
|
||||
import RootStore from "./root.store";
|
||||
|
||||
export type Product = {
|
||||
id: string,
|
||||
number: number,
|
||||
manufactory: string,
|
||||
oem: string,
|
||||
stock: number,
|
||||
receipt_date: string,
|
||||
name: string,
|
||||
cost: number,
|
||||
image: string[],
|
||||
discount: boolean,
|
||||
weight?: number,
|
||||
properties?: ProductProperty[]
|
||||
}
|
||||
|
||||
export type ProductProperty = {
|
||||
id: string,
|
||||
name: string,
|
||||
value: string | string[]
|
||||
}
|
||||
|
||||
export enum ProductSortTypes {
|
||||
byName,
|
||||
byCost,
|
||||
byQty,
|
||||
}
|
||||
|
||||
export enum ProductFilterTypes {
|
||||
byCategory,
|
||||
byName,
|
||||
byCost,
|
||||
byOem,
|
||||
byManufactory,
|
||||
byStock,
|
||||
byDiscount
|
||||
}
|
||||
|
||||
export class ProductStore {
|
||||
private rootStore: RootStore
|
||||
|
||||
private _products: Product[] = []
|
||||
public get products() {
|
||||
return this._products
|
||||
}
|
||||
|
||||
private _isLoading = false
|
||||
public get isLoading() {
|
||||
return this._isLoading
|
||||
}
|
||||
|
||||
private _selectedSroting: ProductSortTypes = ProductSortTypes.byName
|
||||
public get selectedSroting(): ProductSortTypes {
|
||||
return this._selectedSroting
|
||||
}
|
||||
public set selectedSorting(value: ProductSortTypes) {
|
||||
this._selectedSroting = value
|
||||
}
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
this.rootStore = rootStore
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
updateProductFromServer = async () => {
|
||||
try {
|
||||
this._isLoading = true
|
||||
const res = await this.fetchProductFromServer()
|
||||
runInAction(() => {
|
||||
this._products = res
|
||||
this._isLoading = false
|
||||
})
|
||||
} catch {
|
||||
this._isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchProductFromServer(): Promise<Product[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
getFullImageLinks = async (productId: string) => {
|
||||
const res = await this.fetchFullImageLinksFromServer(productId)
|
||||
return res
|
||||
}
|
||||
|
||||
getProductDetailed = async(id: string| undefined) => {
|
||||
if (id) {
|
||||
const res = await this.fetchProductDetailedFromServer(id)
|
||||
return res
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private async fetchProductDetailedFromServer(id: string): Promise<Product> {
|
||||
// const main = serverProducts.find(product => product.id === id)
|
||||
// const properties = detailedParams
|
||||
// if (main) main.properties = properties
|
||||
return {} as Product
|
||||
}
|
||||
|
||||
private async fetchFullImageLinksFromServer(productId: string) {
|
||||
return this._products.find( product => product.id === productId)?.image
|
||||
}
|
||||
|
||||
mapFromTable(tableItem: TableAdapter) {
|
||||
const product: Product = {
|
||||
id: tableItem.id,
|
||||
number: tableItem.number,
|
||||
manufactory: tableItem.manufactory,
|
||||
oem: tableItem.oem,
|
||||
stock: tableItem.stock,
|
||||
receipt_date: tableItem.receipt_date,
|
||||
name: tableItem.name,
|
||||
cost: tableItem.cost,
|
||||
image: tableItem.image,
|
||||
discount: tableItem.discount,
|
||||
}
|
||||
return product
|
||||
}
|
||||
|
||||
mapToTable(products: Product[], cartProducts: CartProduct[]): TableAdapter[] {
|
||||
return this.addQtyFromCardData(products, cartProducts)
|
||||
}
|
||||
|
||||
mapToGrid(products: Product[], cartProducts: CartProduct[]): GridAdapter[] {
|
||||
return this.addQtyFromCardData(products, cartProducts)
|
||||
}
|
||||
|
||||
private addQtyFromCardData(products: Product[], cartProducts: CartProduct[]) {
|
||||
return products.map((product) => {
|
||||
const cart = cartProducts.find(cart => cart.id === product.id)
|
||||
if (cart) return { ...product, qty: cart.qty }
|
||||
return { ...product, qty: 0 }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -23,13 +23,13 @@ export class RecordingsStore {
|
||||
timezone: z.string(),
|
||||
})
|
||||
|
||||
private _recordToPlay: RecordForPlay = {}
|
||||
public get recordToPlay(): RecordForPlay {
|
||||
return this._recordToPlay
|
||||
}
|
||||
public set recordToPlay(value: RecordForPlay) {
|
||||
this._recordToPlay = value
|
||||
}
|
||||
// private _recordToPlay: RecordForPlay = {}
|
||||
// public get recordToPlay(): RecordForPlay {
|
||||
// return this._recordToPlay
|
||||
// }
|
||||
// public set recordToPlay(value: RecordForPlay) {
|
||||
// this._recordToPlay = value
|
||||
// }
|
||||
getFullRecordForPlay(value: RecordForPlay) {
|
||||
return this._recordingSchema.safeParse(value)
|
||||
}
|
||||
@ -49,19 +49,19 @@ export class RecordingsStore {
|
||||
this._cameraIdParam = value
|
||||
}
|
||||
|
||||
private _selectedHost: GetFrigateHost | undefined
|
||||
public get selectedHost(): GetFrigateHost | undefined {
|
||||
return this._selectedHost
|
||||
private _filteredHost: GetFrigateHost | undefined
|
||||
public get filteredHost(): GetFrigateHost | undefined {
|
||||
return this._filteredHost
|
||||
}
|
||||
public set selectedHost(value: GetFrigateHost | undefined) {
|
||||
this._selectedHost = value
|
||||
public set filteredHost(value: GetFrigateHost | undefined) {
|
||||
this._filteredHost = value
|
||||
}
|
||||
private _selectedCamera: GetCameraWHostWConfig | undefined
|
||||
public get selectedCamera(): GetCameraWHostWConfig | undefined {
|
||||
return this._selectedCamera
|
||||
private _filteredCamera: GetCameraWHostWConfig | undefined
|
||||
public get filteredCamera(): GetCameraWHostWConfig | undefined {
|
||||
return this._filteredCamera
|
||||
}
|
||||
public set selectedCamera(value: GetCameraWHostWConfig | undefined) {
|
||||
this._selectedCamera = value
|
||||
public set filteredCamera(value: GetCameraWHostWConfig | undefined) {
|
||||
this._filteredCamera = value
|
||||
}
|
||||
private _selectedRange: [Date | null, Date | null] = [null, null]
|
||||
public get selectedRange(): [Date | null, Date | null] {
|
||||
@ -70,4 +70,12 @@ export class RecordingsStore {
|
||||
public set selectedRange(value: [Date | null, Date | null]) {
|
||||
this._selectedRange = value
|
||||
}
|
||||
|
||||
private _openedCamera: GetCameraWHostWConfig | undefined
|
||||
public get openedCamera(): GetCameraWHostWConfig | undefined {
|
||||
return this._openedCamera
|
||||
}
|
||||
public set openedCamera(value: GetCameraWHostWConfig | undefined) {
|
||||
this._openedCamera = value
|
||||
}
|
||||
}
|
||||
@ -1,39 +1,18 @@
|
||||
import { CartStore } from "./cart.store";
|
||||
import { CategoryStore } from "./category.store";
|
||||
import { FiltersStore } from "./filters/filters.store";
|
||||
import { ModalStore } from "./modal.store";
|
||||
import { OrdersStore } from "./orders.store";
|
||||
import { ProductStore } from "./product.store";
|
||||
import { RecordingsStore } from "./recordings.store";
|
||||
import { SettingsStore } from "./settings.store";
|
||||
import { SideBarsStore } from "./sidebars.store";
|
||||
import PostStore from "./test.store";
|
||||
import { UserStore } from "./user.store";
|
||||
|
||||
class RootStore {
|
||||
userStore: UserStore
|
||||
productStore: ProductStore
|
||||
cartStore: CartStore
|
||||
postStore: PostStore
|
||||
modalStore: ModalStore
|
||||
categoryStore: CategoryStore
|
||||
filtersStore: FiltersStore
|
||||
sideBarsStore: SideBarsStore
|
||||
recordingsStore: RecordingsStore
|
||||
ordersStore: OrdersStore
|
||||
settingsStore: SettingsStore
|
||||
constructor() {
|
||||
this.userStore = new UserStore()
|
||||
this.productStore = new ProductStore(this)
|
||||
this.cartStore = new CartStore()
|
||||
this.postStore = new PostStore()
|
||||
this.modalStore = new ModalStore(this)
|
||||
this.categoryStore = new CategoryStore()
|
||||
this.filtersStore = new FiltersStore()
|
||||
this.sideBarsStore = new SideBarsStore()
|
||||
this.recordingsStore = new RecordingsStore()
|
||||
this.ordersStore = new OrdersStore()
|
||||
this.settingsStore = new SettingsStore()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
|
||||
|
||||
export class SettingsStore {
|
||||
}
|
||||
@ -7,7 +7,6 @@ export class SideBarsStore {
|
||||
return this._rightVisible
|
||||
}
|
||||
public set rightVisible(visible: boolean) {
|
||||
console.log(`set rightVisible`, visible)
|
||||
this._rightVisible = visible
|
||||
}
|
||||
private _leftVisible: boolean = true
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import axios from "axios"
|
||||
import { makeAutoObservable, runInAction } from "mobx"
|
||||
|
||||
|
||||
//todo delete
|
||||
type Posts = {
|
||||
userId: number,
|
||||
id: number,
|
||||
title: string,
|
||||
body: string
|
||||
}
|
||||
|
||||
const getPosts = async () =>
|
||||
(await axios.get<Posts[]>("https://jsonplaceholder.typicode.com/posts")).data
|
||||
|
||||
|
||||
class PostStore {
|
||||
posts: Posts[] = []
|
||||
isLoading = false
|
||||
constructor() {
|
||||
makeAutoObservable(this)
|
||||
}
|
||||
|
||||
getPostsAction = async () => {
|
||||
try {
|
||||
this.isLoading = true
|
||||
const res = await getPosts()
|
||||
runInAction( () => {
|
||||
this.posts = res
|
||||
this.isLoading = false
|
||||
})
|
||||
} catch {
|
||||
this.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PostStore
|
||||
@ -11,9 +11,19 @@ export const strings = {
|
||||
object: 'Объект',
|
||||
duration: 'Длительность',
|
||||
startTime: 'Начало',
|
||||
endTime: 'Конец'
|
||||
endTime: 'Конец',
|
||||
doubleClickToFullHint: 'Двойное нажатие мышью для полноэкранного просмотра',
|
||||
},
|
||||
|
||||
empty: 'Пусто',
|
||||
pleaseSelectRole: 'Пожалуйста выберите роль',
|
||||
nothingHere: 'Ни чего нет',
|
||||
allowed: 'Разрешено',
|
||||
notAllowed: 'Не разрешено',
|
||||
camera: 'Камера',
|
||||
camersDoesNotExist: 'Камер нет',
|
||||
search: 'Поиск',
|
||||
recordings: 'Записи',
|
||||
hour: 'Час',
|
||||
minute: 'Минута',
|
||||
second: 'Секунда',
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import React from 'react';
|
||||
import { CameraConfig } from '../../types/frigateConfig';
|
||||
import { CameraConfig } from '../types/frigateConfig';
|
||||
import { AspectRatio, Button, Card, Flex, Grid, Group, Space, Text, createStyles, useMantineTheme } from '@mantine/core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { routesPath } from '../../router/routes.path';
|
||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
|
||||
import { frigateApi, mapHostToHostname, proxyApi } from '../../services/frigate.proxy/frigate.api';
|
||||
import AutoUpdatedImage from './AutoUpdatedImage';
|
||||
import { recordingsPageQuery } from '../../pages/RecordingsPage';
|
||||
import { routesPath } from '../router/routes.path';
|
||||
import { GetCameraWHostWConfig, GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
|
||||
import { frigateApi, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import AutoUpdatedImage from '../shared/components/images/AutoUpdatedImage';
|
||||
import { recordingsPageQuery } from '../pages/RecordingsPage';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
@ -61,7 +62,7 @@ const CameraCard = ({
|
||||
<Group
|
||||
className={classes.bottomGroup}>
|
||||
<Flex justify='space-evenly' mt='0.5rem' w='100%'>
|
||||
<Button size='sm' onClick={handleOpenRecordings}>Recordings</Button>
|
||||
<Button size='sm' onClick={handleOpenRecordings}>{strings.recordings}</Button>
|
||||
</Flex>
|
||||
</Group>
|
||||
</Card>
|
||||
@ -1,95 +0,0 @@
|
||||
import { Button, Table } from '@mantine/core';
|
||||
import React, { useState } from 'react';
|
||||
import SortedTh from '../shared/components/table.aps/SortedTh';
|
||||
import { DeliveryPoint } from '../shared/stores/user.store';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
|
||||
interface DeliveryPointsTableProps {
|
||||
data: DeliveryPoint[]
|
||||
}
|
||||
|
||||
const DeliveryPointsTable = ({ data }: DeliveryPointsTableProps) => {
|
||||
|
||||
const [tableData, setData] = useState(data)
|
||||
|
||||
const [reversed, setReversed] = useState(false)
|
||||
const [sortedName, setSortedName] = useState<string | null>(null)
|
||||
|
||||
const handleSort = (headName: string, dataIndex: number) => {
|
||||
const reverse = headName === sortedName ? !reversed : false;
|
||||
setReversed(reverse)
|
||||
const keys = Object.keys(data[0]) as Array<keyof DeliveryPoint>
|
||||
const key = keys[dataIndex]
|
||||
if (reverse) {
|
||||
setData(sortByKey(data, key).reverse())
|
||||
} else {
|
||||
setData(sortByKey(data, key))
|
||||
}
|
||||
setSortedName(headName)
|
||||
}
|
||||
|
||||
function sortByKey<T, K extends keyof T>(array: T[], key: K): T[] {
|
||||
return array.sort((a, b) => {
|
||||
let valueA = a[key];
|
||||
let valueB = b[key];
|
||||
|
||||
const stringValueA = String(valueA).toLowerCase();
|
||||
const stringValueB = String(valueB).toLowerCase();
|
||||
|
||||
if (stringValueA < stringValueB) return -1;
|
||||
if (stringValueA > stringValueB) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
const headTitle = [
|
||||
{ propertyIndex: 1, title: strings.name },
|
||||
{ propertyIndex: 2, title: strings.schedule },
|
||||
{ propertyIndex: 3, title: strings.address },
|
||||
{ title: '', sorting: false },
|
||||
]
|
||||
|
||||
const tableHead = headTitle.map(head => {
|
||||
return (
|
||||
<SortedTh
|
||||
key={uuidv4()}
|
||||
title={head.title}
|
||||
reversed={reversed}
|
||||
sortedName={sortedName}
|
||||
onSort={() => handleSort(head.title, head.propertyIndex ? head.propertyIndex : 0)}
|
||||
sorting={head.sorting} />
|
||||
)
|
||||
})
|
||||
|
||||
const rows = tableData.map(item => {
|
||||
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.schedule}</td>
|
||||
<td>{item.address}</td>
|
||||
<td><Button>{strings.edit}</Button></td>
|
||||
</tr>
|
||||
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table >
|
||||
<thead>
|
||||
<tr>
|
||||
{tableHead}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeliveryPointsTable;
|
||||
@ -4,12 +4,13 @@ import SortedTh from '../shared/components/table.aps/SortedTh';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { IconBulbFilled, IconBulbOff, IconDeviceFloppy, IconPencil, IconPlus, IconSettings, IconTrash } from '@tabler/icons-react';
|
||||
import SwitchCell from '../shared/components/hosts.table/SwitchCell';
|
||||
import TextInputCell from '../shared/components/hosts.table/TextInputCell';
|
||||
import SwitchCell from './hosts.table/SwitchCell';
|
||||
import TextInputCell from './hosts.table/TextInputCell';
|
||||
import ObjectId from 'bson-objectid';
|
||||
import { debounce } from '../shared/utils/debounce';
|
||||
import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
|
||||
import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
|
||||
import StateCell from './hosts.table/StateCell';
|
||||
|
||||
interface TableProps<T> {
|
||||
data: T[],
|
||||
@ -123,10 +124,11 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
|
||||
<tr key={item.id}>
|
||||
<TextInputCell text={item.name} width='40%' id={item.id} propertyName='name' onChange={handleTextChange} />
|
||||
<TextInputCell text={item.host} width='40%' id={item.id} propertyName='host' onChange={handleTextChange} />
|
||||
<SwitchCell value={item.enabled} width='10%' id={item.id} propertyName='enabled' toggle={handleSwitchChange} />
|
||||
<SwitchCell value={item.enabled} width='5%' id={item.id} propertyName='enabled' toggle={handleSwitchChange} />
|
||||
<StateCell id={item.id} width='5%' />
|
||||
<td align='right' style={{ width: '10%', padding: '0', }}>
|
||||
<Flex justify='center'>
|
||||
<HostSettingsMenu id={item.id} />
|
||||
<HostSettingsMenu host={item} />
|
||||
<Button size='xs' onClick={() => handleDeleteRow(item.id)}><IconTrash /></Button>
|
||||
</Flex>
|
||||
</td>
|
||||
|
||||
@ -6,7 +6,7 @@ import { LivePlayerMode } from '../types/live';
|
||||
import useCameraActivity from '../hooks/use-camera-activity';
|
||||
import useCameraLiveMode from '../hooks/use-camera-live-mode';
|
||||
import WebRtcPlayer from '../shared/components/players/WebRTCPlayer';
|
||||
import { Flex } from '@mantine/core';
|
||||
import { AspectRatio, Flex } from '@mantine/core';
|
||||
import { frigateApi, proxyApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { GetCameraWHostWConfig } from '../services/frigate.proxy/frigate.schema';
|
||||
|
||||
@ -88,15 +88,13 @@ const Player = ({
|
||||
player = (
|
||||
<JSMpegPlayer
|
||||
wsUrl={wsUrl}
|
||||
cameraWidth={camera.config?.detect.width}
|
||||
cameraHeight={camera.config?.detect.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex w='100%' h='100%' justify='center'>
|
||||
{player}
|
||||
</Flex>
|
||||
);
|
||||
return player ? player : null
|
||||
}
|
||||
|
||||
export default Player;
|
||||
@ -1,156 +0,0 @@
|
||||
import { Table, } from '@mantine/core';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useDisclosure, useHotkeys, } from '@mantine/hooks';
|
||||
import TableRow from '../shared/components/table.aps/TableRow';
|
||||
import InputModal from '../shared/components/InputModal';
|
||||
import { Context } from '..';
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import ProductsTableHead from '../shared/components/table.aps/ProductsTableHead';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
export type TableAdapter = {
|
||||
id: string,
|
||||
number: number,
|
||||
manufactory: string,
|
||||
oem: string,
|
||||
stock: number,
|
||||
receipt_date: string,
|
||||
name: string,
|
||||
cost: number,
|
||||
image: string[],
|
||||
discount: boolean,
|
||||
qty: number
|
||||
}
|
||||
|
||||
interface ProductTableProps {
|
||||
tableData?: TableAdapter[],
|
||||
showDelete?: boolean
|
||||
}
|
||||
|
||||
|
||||
const ProductTable = ({tableData, showDelete}: ProductTableProps) => {
|
||||
|
||||
const { cartStore } = useContext(Context)
|
||||
const { sortCart } = cartStore
|
||||
|
||||
const data = tableData ? tableData : []
|
||||
|
||||
const [sortBy, setSortBy] = useState<string | null>(null);
|
||||
const [reverseSortDirection, setReverseSortDirection] = useState(false);
|
||||
|
||||
// const [data, setData] = useState(tableData)
|
||||
|
||||
const [selectedId, setSelectedId] = useState<string>("")
|
||||
const [qtyValue, setQtyValue] = useState<number>(0)
|
||||
|
||||
const setSorting = (title: string) => {
|
||||
const reversed = title === sortBy ? !reverseSortDirection : false;
|
||||
setReverseSortDirection(reversed)
|
||||
setSortBy(title)
|
||||
sortCart(reversed)
|
||||
}
|
||||
|
||||
const handleUpDown = (key: string) => {
|
||||
if (!selectedId) setSelectedId(data[0].id)
|
||||
if (selectedId) {
|
||||
const currentIndex = data.findIndex(value => value.id === selectedId)
|
||||
switch (key) {
|
||||
case "up": {
|
||||
if (currentIndex !== 0) setSelectedId(data[currentIndex - 1].id)
|
||||
break
|
||||
}
|
||||
case "down": {
|
||||
if (currentIndex !== data.length - 1) setSelectedId(data[currentIndex + 1].id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeftRight = (key: string) => {
|
||||
if (!selectedId) setSelectedId(data[0].id)
|
||||
if (selectedId) {
|
||||
if (key === 'right') {
|
||||
increaseData(selectedId)
|
||||
}
|
||||
if (key === 'left') {
|
||||
decreaseData(selectedId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const increaseData = (selectedId: string) => {
|
||||
setQtyData(selectedId, qtyValue+1)
|
||||
}
|
||||
|
||||
const decreaseData = (selectedId: string) => {
|
||||
if (qtyValue > 0) setQtyData(selectedId, qtyValue-1)
|
||||
}
|
||||
|
||||
const setQtyData = (selectedId: string, value: number) => {
|
||||
// setData(data.map(element => {
|
||||
// if (element.id === selectedId) return {
|
||||
// ...element,
|
||||
// qty: value
|
||||
// }
|
||||
// return element
|
||||
// }))
|
||||
const tableItem = data.find( item => item.id === selectedId)
|
||||
if (tableItem) cartStore.setToCart(tableItem, value)
|
||||
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
cartStore.deleteFromCart(id)
|
||||
}
|
||||
|
||||
const handleOpenInputQty = () => {
|
||||
if (selectedId) open()
|
||||
}
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false)
|
||||
|
||||
const handleInputModalValue = (value: number) => {
|
||||
setQtyData(selectedId, value)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (data.length !== 0 && selectedId) {
|
||||
const qty = data.find( (element) => element.id === selectedId)?.qty || 0
|
||||
setQtyValue(qty)
|
||||
}
|
||||
}, [selectedId, data])
|
||||
|
||||
useHotkeys([
|
||||
['ArrowUp', () => handleUpDown('up')],
|
||||
['ArrowDown', () => handleUpDown('down')],
|
||||
['ArrowRight', () => handleLeftRight('right')],
|
||||
['ArrowLeft', () => handleLeftRight('left')],
|
||||
['mod+Enter', () => handleOpenInputQty()],
|
||||
])
|
||||
|
||||
const rows = data.map(element =>
|
||||
<TableRow
|
||||
key={element.id}
|
||||
element={element}
|
||||
selected={selectedId}
|
||||
setQty={setQtyData}
|
||||
onDelete={handleDelete}
|
||||
showDelete={showDelete}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputModal key={uuidv4()} inValue={qtyValue} putValue={handleInputModalValue} opened={opened} open={open} close={close} />
|
||||
<Table >
|
||||
<ProductsTableHead reverseSortDirection={reverseSortDirection} sortBy={sortBy} setSorting={setSorting} />
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductTable;
|
||||
@ -16,12 +16,12 @@ const RecordingsFiltersRightSide = ({
|
||||
return (
|
||||
<>
|
||||
<HostSelectFilter />
|
||||
{recStore.selectedHost ?
|
||||
{recStore.filteredHost ?
|
||||
<CameraSelectFilter
|
||||
selectedHostId={recStore.selectedHost.id} />
|
||||
selectedHostId={recStore.filteredHost.id} />
|
||||
: <></>
|
||||
}
|
||||
{recStore.selectedCamera ?
|
||||
{recStore.filteredCamera ?
|
||||
<DateRangeSelectFilter />
|
||||
: <></>
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { dateToQueryString, getResolvedTimeZone } from '../shared/utils/dateUtil
|
||||
import { Context } from '..';
|
||||
import { Flex, Text } from '@mantine/core';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import DayAccordion from '../shared/components/accordion/DayAccordion';
|
||||
|
||||
@ -18,11 +18,11 @@ const SelecteDayList = ({
|
||||
day
|
||||
}: SelecteDayListProps) => {
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
const camera = recStore.selectedCamera
|
||||
const host = recStore.selectedHost
|
||||
const camera = recStore.filteredCamera
|
||||
const host = recStore.filteredHost
|
||||
|
||||
const { data, isPending, isError, refetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.selectedCamera?.id, day],
|
||||
queryKey: [frigateQueryKeys.getRecordingsSummary, recStore.filteredCamera?.id, day],
|
||||
queryFn: async () => {
|
||||
if (camera && host) {
|
||||
const stringDay = dateToQueryString(day)
|
||||
@ -35,7 +35,7 @@ const SelecteDayList = ({
|
||||
})
|
||||
|
||||
const handleRetry = () => {
|
||||
if (recStore.selectedHost) refetch()
|
||||
if (recStore.filteredHost) refetch()
|
||||
}
|
||||
|
||||
if (isPending) return <CenterLoader />
|
||||
|
||||
@ -8,23 +8,21 @@ import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.
|
||||
import { host } from '../shared/env.const';
|
||||
import CogwheelLoader from '../shared/components/loaders/CogwheelLoader';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
|
||||
interface SelectedCameraListProps {
|
||||
// cameraId: string,
|
||||
}
|
||||
|
||||
const SelectedCameraList = ({
|
||||
// cameraId,
|
||||
}: SelectedCameraListProps) => {
|
||||
|
||||
const { recordingsStore: recStore } = useContext(Context)
|
||||
|
||||
const { data: camera, isPending: cameraPending, isError: cameraError, refetch: cameraRefetch } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getCameraWHost, recStore.selectedCamera?.id],
|
||||
queryKey: [frigateQueryKeys.getCameraWHost, recStore.filteredCamera?.id],
|
||||
queryFn: async () => {
|
||||
if (recStore.selectedCamera) {
|
||||
return frigateApi.getCameraWHost(recStore.selectedCamera.id)
|
||||
if (recStore.filteredCamera) {
|
||||
return frigateApi.getCameraWHost(recStore.filteredCamera.id)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -42,12 +40,8 @@ const SelectedCameraList = ({
|
||||
return (
|
||||
<Flex w='100%' h='100%' direction='column' align='center'>
|
||||
<Text>{camera.frigateHost.name} / {camera.name}</Text>
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
<Suspense>
|
||||
<CameraAccordion camera={camera} host={camera.frigateHost} />
|
||||
<CameraAccordion />
|
||||
</Suspense>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@ -4,7 +4,7 @@ import { host } from '../shared/env.const';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { frigateQueryKeys, frigateApi } from '../services/frigate.proxy/frigate.api';
|
||||
import { Context } from '..';
|
||||
import CenterLoader from '../shared/components/CenterLoader';
|
||||
import CenterLoader from '../shared/components/loaders/CenterLoader';
|
||||
import RetryErrorPage from '../pages/RetryErrorPage';
|
||||
import { strings } from '../shared/strings/strings';
|
||||
const CameraAccordion = lazy(() => import('../shared/components/accordion/CameraAccordion'));
|
||||
@ -33,10 +33,11 @@ const SelectedHostList = ({
|
||||
|
||||
const handleOnChange = (cameraId: string | null) => {
|
||||
setOpenCameraId(openCameraId === cameraId ? null : cameraId)
|
||||
recStore.openedCamera = host?.cameras.find( camera => camera.id === cameraId)
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
if (recStore.selectedHost) hostRefetch()
|
||||
if (recStore.filteredHost) hostRefetch()
|
||||
}
|
||||
|
||||
if (hostPending) return <CenterLoader />
|
||||
@ -44,14 +45,14 @@ const SelectedHostList = ({
|
||||
|
||||
if (!host || host.cameras.length < 1) return null
|
||||
|
||||
const cameras = host.cameras.slice(0, 2).map(camera => {
|
||||
const cameras = host.cameras.map(camera => {
|
||||
return (
|
||||
<Accordion.Item key={camera.id + 'Item'} value={camera.id}>
|
||||
<Accordion.Control key={camera.id + 'Control'}>{strings.camera}: {camera.name}</Accordion.Control>
|
||||
<Accordion.Panel key={camera.id + 'Panel'}>
|
||||
{openCameraId === camera.id && (
|
||||
<Suspense>
|
||||
<CameraAccordion camera={camera} host={host} />
|
||||
<CameraAccordion />
|
||||
</Suspense>
|
||||
)}
|
||||
</Accordion.Panel>
|
||||
|
||||
@ -3,10 +3,10 @@ import { useDisclosure, useMediaQuery } from "@mantine/hooks";
|
||||
import UserMenu from '../../shared/components/UserMenu';
|
||||
import { useAuth } from 'react-oidc-context';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ColorSchemeToggle from "../../shared/components/ColorSchemeToggle";
|
||||
import Logo from "../../shared/components/Logo";
|
||||
import ColorSchemeToggle from "../../shared/components/buttons/ColorSchemeToggle";
|
||||
import Logo from "../../shared/components/images/LogoImage";
|
||||
import { routesPath } from "../../router/routes.path";
|
||||
import DrawerMenu from "../../shared/components/DrawerMenu";
|
||||
import DrawerMenu from "../../shared/components/menu/DrawerMenu";
|
||||
|
||||
const HEADER_HEIGHT = rem(60)
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import { HeaderActionProps, LinkItem } from "./HeaderAction";
|
||||
|
||||
export const headerLinks: LinkItem[] = [
|
||||
{ link: routesPath.MAIN_PATH, label: headerMenu.home },
|
||||
{ link: routesPath.TEST_PATH, label: headerMenu.test },
|
||||
{ link: routesPath.SETTINGS_PATH, label: headerMenu.settings },
|
||||
{ link: routesPath.RECORDINGS_PATH, label: headerMenu.recordings },
|
||||
{ link: routesPath.HOSTS_PATH, label: headerMenu.hostsConfig },
|
||||
|
||||
34
src/widgets/hosts.table/StateCell.tsx
Normal file
34
src/widgets/hosts.table/StateCell.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Flex, Paper } from '@mantine/core';
|
||||
import { IconPower } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
interface StateCellProps {
|
||||
id?: string
|
||||
width?: string,
|
||||
}
|
||||
const StateCell = ({
|
||||
id,
|
||||
width,
|
||||
}: StateCellProps) => {
|
||||
const { isPending, isError, data } = useQuery({
|
||||
queryKey: [frigateQueryKeys.getFrigateHosts, id],
|
||||
queryFn: frigateApi.getHosts,
|
||||
staleTime: 60 * 1000,
|
||||
gcTime: Infinity,
|
||||
refetchInterval: 30 * 1000,
|
||||
})
|
||||
|
||||
const state = data?.find(host => host.id === id)?.state
|
||||
|
||||
return (
|
||||
<td style={{ width: width }}>
|
||||
<Flex w='100%' justify='center'>
|
||||
<IconPower color={state ? 'green' : 'red'}/>
|
||||
</Flex>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export default StateCell;
|
||||
12
yarn.lock
12
yarn.lock
@ -8329,6 +8329,13 @@ react-dev-utils@^12.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
react-device-detect@^2.2.3:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-device-detect/-/react-device-detect-2.2.3.tgz#97a7ae767cdd004e7c3578260f48cf70c036e7ca"
|
||||
integrity sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==
|
||||
dependencies:
|
||||
ua-parser-js "^1.0.33"
|
||||
|
||||
react-dom@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
@ -9627,6 +9634,11 @@ typescript@^4.4.2:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
ua-parser-js@^1.0.33:
|
||||
version "1.0.37"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f"
|
||||
integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user