diff --git a/web-new/src/components/card/MiniEventCard.tsx b/web-new/src/components/card/MiniEventCard.tsx new file mode 100644 index 000000000..fa5aeefeb --- /dev/null +++ b/web-new/src/components/card/MiniEventCard.tsx @@ -0,0 +1,83 @@ +import { useApiHost } from "@/api"; +import { Card } from "../ui/card"; +import { Event as FrigateEvent } from "@/types/event"; +import { LuClock, LuStar } from "react-icons/lu"; +import { useCallback } from "react"; +import TimeAgo from "../dynamic/TimeAgo"; +import { HiOutlineVideoCamera } from "react-icons/hi"; +import { MdOutlineLocationOn } from "react-icons/md"; +import axios from "axios"; + +type MiniEventCardProps = { + event: FrigateEvent; + onUpdate?: () => void; +}; + +export default function MiniEventCard({ event, onUpdate }: MiniEventCardProps) { + const baseUrl = useApiHost(); + const onSave = useCallback( + async (e: Event) => { + e.stopPropagation(); + let response; + if (!event.retain_indefinitely) { + response = await axios.post(`events/${event.id}/retain`); + } else { + response = await axios.delete(`events/${event.id}/retain`); + } + if (response.status === 200 && onUpdate) { + onUpdate(); + } + }, + [event] + ); + + return ( + +
+
+ onSave(e)} + fill={event.retain_indefinitely ? "currentColor" : "none"} + /> + {event.end_time ? null : ( +
+ In progress +
+ )} +
+
+
+ {event.label.replaceAll("_", " ")} + {event.sub_label + ? `: ${event.sub_label.replaceAll("_", " ")}` + : null} +
+
+
+ +
+ +
+
+
+ + {event.camera.replaceAll("_", " ")} +
+ {event.zones.length ? ( +
+ + {event.zones.join(", ").replaceAll("_", " ")} +
+ ) : null} +
+
+
+
+ ); +} diff --git a/web-new/src/components/dynamic/TimeAgo.tsx b/web-new/src/components/dynamic/TimeAgo.tsx new file mode 100644 index 000000000..3dd510cef --- /dev/null +++ b/web-new/src/components/dynamic/TimeAgo.tsx @@ -0,0 +1,83 @@ +import { FunctionComponent, useEffect, useMemo, useState } from "react"; + +interface IProp { + /** The time to calculate time-ago from */ + time: number; + /** OPTIONAL: overwrite current time */ + currentTime?: Date; + /** OPTIONAL: boolean that determines whether to show the time-ago text in dense format */ + dense?: boolean; + /** OPTIONAL: set custom refresh interval in milliseconds, default 1000 (1 sec) */ + refreshInterval?: number; +} + +type TimeUnit = { + unit: string; + full: string; + value: number; +}; + +const timeAgo = ({ time, currentTime = new Date(), dense = false }: IProp): string => { + if (typeof time !== 'number' || time < 0) return 'Invalid Time Provided'; + + const pastTime: Date = new Date(time); + const elapsedTime: number = currentTime.getTime() - pastTime.getTime(); + + const timeUnits: TimeUnit[] = [ + { unit: 'yr', full: 'year', value: 31536000 }, + { unit: 'mo', full: 'month', value: 0 }, + { unit: 'd', full: 'day', value: 86400 }, + { unit: 'h', full: 'hour', value: 3600 }, + { unit: 'm', full: 'minute', value: 60 }, + { unit: 's', full: 'second', value: 1 }, + ]; + + const elapsed: number = elapsedTime / 1000; + if (elapsed < 10) { + return 'just now'; + } + + for (let i = 0; i < timeUnits.length; i++) { + // if months + if (i === 1) { + // Get the month and year for the time provided + const pastMonth = pastTime.getUTCMonth(); + const pastYear = pastTime.getUTCFullYear(); + + // get current month and year + const currentMonth = currentTime.getUTCMonth(); + const currentYear = currentTime.getUTCFullYear(); + + let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth); + + // check if the time provided is the previous month but not exceeded 1 month ago. + if (currentTime.getUTCDate() < pastTime.getUTCDate()) { + monthDiff--; + } + + if (monthDiff > 0) { + const unitAmount = monthDiff; + return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + } + } else if (elapsed >= timeUnits[i].value) { + const unitAmount: number = Math.floor(elapsed / timeUnits[i].value); + return `${unitAmount}${dense ? timeUnits[i].unit : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + } + } + return 'Invalid Time'; +}; + +const TimeAgo: FunctionComponent = ({ refreshInterval = 1000, ...rest }): JSX.Element => { + const [currentTime, setCurrentTime] = useState(new Date()); + useEffect(() => { + const intervalId: NodeJS.Timeout = setInterval(() => { + setCurrentTime(new Date()); + }, refreshInterval); + return () => clearInterval(intervalId); + }, [refreshInterval]); + + const timeAgoValue = useMemo(() => timeAgo({ currentTime, ...rest }), [currentTime, rest]); + + return {timeAgoValue}; +}; +export default TimeAgo; diff --git a/web-new/src/pages/Dashboard.tsx b/web-new/src/pages/Dashboard.tsx index cc1f19970..b214aef40 100644 --- a/web-new/src/pages/Dashboard.tsx +++ b/web-new/src/pages/Dashboard.tsx @@ -17,10 +17,23 @@ import { AiOutlinePicture } from "react-icons/ai"; import { FaWalking } from "react-icons/fa"; import { LuEar } from "react-icons/lu"; import { TbMovie } from "react-icons/tb"; +import MiniEventCard from "@/components/card/MiniEventCard"; +import { Event } from "@/types/event"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; export function Dashboard() { const { data: config } = useSWR("config"); + const recentTimestamp = useMemo(() => { + const now = new Date(); + now.setMinutes(now.getMinutes() - 30); + return now.getTime() / 1000; + }, []); + const { data: events, mutate: updateEvents } = useSWR([ + "events", + { limit: 10, after: recentTimestamp }, + ]); + const sortedCameras = useMemo(() => { if (!config) { return []; @@ -39,7 +52,27 @@ export function Dashboard() { {config && (
-
+ {events && events.length > 0 && ( + <> + Recent Events + +
+ {events.map((event) => { + return ( + updateEvents()} + /> + ); + })} +
+ +
+ + )} + Cameras +
{sortedCameras.map((camera) => { return ; })} @@ -73,9 +106,9 @@ function Camera({ camera }: { camera: CameraConfig }) {
- +
{camera.name.replaceAll("_", " ")} - +