Browse Source

feat(web): activity indicator while loading

Paul Armstrong 4 years ago
parent
commit
45526a7652

+ 6 - 3
web/src/App.jsx

@@ -1,4 +1,5 @@
 import { h } from 'preact';
+import ActivityIndicator from './components/ActivityIndicator';
 import Camera from './Camera';
 import CameraMap from './CameraMap';
 import Cameras from './Cameras';
@@ -7,12 +8,14 @@ import Event from './Event';
 import Events from './Events';
 import { Router } from 'preact-router';
 import Sidebar from './Sidebar';
-import Api, { useConfig } from './api';
+import Api, { FetchStatus, useConfig } from './api';
 
 export default function App() {
   const { data, status } = useConfig();
-  return !data ? (
-    <div />
+  return status !== FetchStatus.LOADED ? (
+    <div className="flex flex-grow-1 min-h-screen justify-center items-center">
+      <ActivityIndicator />
+    </div>
   ) : (
     <div className="md:flex flex-col md:flex-row md:min-h-screen w-full bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white">
       <Sidebar />

+ 1 - 0
web/src/Cameras.jsx

@@ -1,4 +1,5 @@
 import { h } from 'preact';
+import ActivityIndicator from './components/ActivityIndicator';
 import Box from './components/Box';
 import CameraImage from './components/CameraImage';
 import Events from './Events';

+ 4 - 3
web/src/Debug.jsx

@@ -1,9 +1,10 @@
 import { h } from 'preact';
+import ActivityIndicator from './components/ActivityIndicator';
 import Box from './components/Box';
 import Button from './components/Button';
 import Heading from './components/Heading';
 import Link from './components/Link';
-import { useConfig, useStats } from './api';
+import { FetchStatus, useConfig, useStats } from './api';
 import { Table, Tbody, Thead, Tr, Th, Td } from './components/Table';
 import { useCallback, useEffect, useState } from 'preact/hooks';
 
@@ -27,8 +28,8 @@ export default function Debug() {
   }, [timeoutId]);
   const { data: stats, status } = useStats(null, timeoutId);
 
-  if (!stats) {
-    return 'loading…';
+  if (stats === null && (status === FetchStatus.LOADING || status === FetchStatus.NONE)) {
+    return <ActivityIndicator />;
   }
 
   const { detectors, detection_fps, service, ...cameras } = stats;

+ 5 - 9
web/src/Event.jsx

@@ -1,21 +1,17 @@
 import { h, Fragment } from 'preact';
+import ActivityIndicator from './components/ActivityIndicator';
 import Box from './components/Box';
 import Heading from './components/Heading';
 import Link from './components/Link';
+import { FetchStatus, useApiHost, useEvent } from './api';
 import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
-import { useApiHost, useEvent } from './api';
 
 export default function Event({ eventId }) {
   const apiHost = useApiHost();
-  const { data } = useEvent(eventId);
+  const { data, status } = useEvent(eventId);
 
-  if (!data) {
-    return (
-      <div>
-        <Heading>{eventId}</Heading>
-        <p>loading…</p>
-      </div>
-    );
+  if (status !== FetchStatus.LOADED) {
+    return <ActivityIndicator />;
   }
 
   const startime = new Date(data.start_time * 1000);

+ 4 - 3
web/src/Events.jsx

@@ -1,11 +1,12 @@
 import { h } from 'preact';
+import ActivityIndicator from './components/ActivityIndicator';
 import Box from './components/Box';
 import Heading from './components/Heading';
 import Link from './components/Link';
 import produce from 'immer';
 import { route } from 'preact-router';
+import { FetchStatus, useApiHost, useConfig, useEvents } from './api';
 import { Table, Thead, Tbody, Tfoot, Th, Tr, Td } from './components/Table';
-import { useApiHost, useConfig, useEvents } from './api';
 import { useCallback, useContext, useEffect, useMemo, useRef, useReducer, useState } from 'preact/hooks';
 
 const API_LIMIT = 25;
@@ -194,8 +195,8 @@ export default function Events({ path: pathname } = {}) {
           </Tbody>
           <Tfoot>
             <Tr>
-              <Td className="text-center" colspan="8">
-                {status === 'loading' ? 'Loading…' : reachedEnd ? 'No more events' : null}
+              <Td className="text-center p-4" colspan="8">
+                {status === FetchStatus.LOADING ? <ActivityIndicator /> : reachedEnd ? 'No more events' : null}
               </Td>
             </Tr>
           </Tfoot>

+ 15 - 0
web/src/components/ActivityIndicator.jsx

@@ -0,0 +1,15 @@
+import { h } from 'preact';
+
+const sizes = {
+  sm: 'h-4 w-4 border-2 border-t-2',
+  md: 'h-8 w-8 border-4 border-t-4',
+  lg: 'h-16 w-16 border-8 border-t-8',
+};
+
+export default function ActivityIndicator({ size = 'md' }) {
+  return (
+    <div className="w-full flex items-center justify-center" aria-label="Loading…">
+      <div className={`activityindicator ease-in rounded-full border-gray-200 text-blue-500 ${sizes[size]}`} />
+    </div>
+  );
+}

+ 6 - 1
web/src/components/CameraImage.jsx

@@ -1,4 +1,5 @@
 import { h } from 'preact';
+import ActivityIndicator from './ActivityIndicator';
 import { useApiHost, useConfig } from '../api';
 import { useCallback, useEffect, useContext, useMemo, useRef, useState } from 'preact/hooks';
 
@@ -54,7 +55,11 @@ export default function CameraImage({ camera, onload, searchParams = '' }) {
 
   return (
     <div ref={containerRef}>
-      {loadedSrc ? <img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} /> : null}
+      {loadedSrc ? (
+        <img width={scaledHeight * aspectRatio} height={scaledHeight} src={loadedSrc} alt={name} />
+      ) : (
+        <ActivityIndicator />
+      )}
     </div>
   );
 }

+ 24 - 0
web/src/index.css

@@ -1,3 +1,27 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
+
+.activityindicator {
+  border-top-color: currentColor;
+  -webkit-animation: spinner 0.75s linear infinite;
+  animation: spinner 0.75s linear infinite;
+}
+
+@-webkit-keyframes spinner {
+  0% {
+    -webkit-transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(360deg);
+  }
+}
+
+@keyframes spinner {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}