fix player

add translates
add status
add shared links
add link page
refactor
This commit is contained in:
NlightN22 2024-02-27 03:15:48 +07:00
parent fecd30df70
commit 26c5c6504a
95 changed files with 629 additions and 2144 deletions

View File

@ -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",

View File

@ -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" />
<!--

View File

@ -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 >

View File

@ -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);

View File

@ -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

View File

@ -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';

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View 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;

View File

@ -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)

View File

@ -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';

View File

@ -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);

View File

@ -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',

View File

@ -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 />,

View File

@ -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[] => {

View File

@ -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({

View File

@ -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"
/>
</>

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 '../..';

View File

@ -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>
)

View File

@ -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} />
:

View File

@ -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>

View File

@ -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;

View 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;

View 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;

View File

@ -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 {

View 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;

View File

@ -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}
/>

View File

@ -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 {

View File

@ -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}
/>

View File

@ -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

View File

@ -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';

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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

View File

@ -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 />;

View File

@ -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) => ({

View File

@ -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}>

View File

@ -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) => ({

View File

@ -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: {

View File

@ -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

View File

@ -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';

View File

@ -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;

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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 []
}
}

View File

@ -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"

View File

@ -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
}
}
}

View File

@ -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 () {
}
}

View File

@ -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 }
})
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -1,4 +0,0 @@
export class SettingsStore {
}

View File

@ -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

View File

@ -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

View File

@ -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: 'Секунда',

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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';
@ -26,7 +26,7 @@ const Player = ({
const hostNameWPort = camera.frigateHost ? new URL(camera.frigateHost.host).host : ''
const wsUrl = proxyApi.cameraWsURL(hostNameWPort, camera.name)
const cameraConfig = camera.config!
const { activeMotion, activeAudio, activeTracking } =
useCameraActivity(cameraConfig);
@ -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;

View File

@ -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;

View File

@ -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 />
: <></>
}

View File

@ -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 />

View File

@ -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>
)

View File

@ -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>

View File

@ -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)

View File

@ -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 },

View 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;

View File

@ -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"