From 143fbc52f07ac4c8d8781ef90d50590ea7498407 Mon Sep 17 00:00:00 2001 From: Kevin Zheng Date: Thu, 30 Nov 2023 21:01:43 +1100 Subject: [PATCH] Minor improvements to recipes page --- client/src/components/generic/Snackbar.tsx | 19 +++-- client/src/pages/RecipesPage.tsx | 97 +++++++++++----------- client/src/pages/index.tsx | 13 ++- client/src/slices/loading.ts | 4 +- 4 files changed, 71 insertions(+), 62 deletions(-) diff --git a/client/src/components/generic/Snackbar.tsx b/client/src/components/generic/Snackbar.tsx index 69a5441f..cbe2a613 100644 --- a/client/src/components/generic/Snackbar.tsx +++ b/client/src/components/generic/Snackbar.tsx @@ -12,9 +12,15 @@ import { useState, } from "react"; -const SnackbarContext = createContext< - (message?: string, secondary?: string) => () => void ->(() => noop); +type A = ( + message?: string, + secondary?: string, + options?: { + error?: boolean; + } +) => () => void; + +const SnackbarContext = createContext(() => noop); export interface SnackbarMessage { message?: ReactNode; @@ -51,7 +57,7 @@ export function SnackbarProvider({ children }: { children?: ReactNode }) { }, [snackPack, current, open]); const handleMessage = useCallback( - (message?: string, secondary?: string) => { + ((message?: string, secondary?: string, options = {}) => { setSnackPack((prev) => [ ...prev, { @@ -63,8 +69,11 @@ export function SnackbarProvider({ children }: { children?: ReactNode }) { content: filter([message, secondary]).join(", "), timestamp: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`, })); + if (options.error) { + console.error(`${message}, ${secondary}`); + } return () => handleClose(""); - }, + }) as A, [setSnackPack] ); diff --git a/client/src/pages/RecipesPage.tsx b/client/src/pages/RecipesPage.tsx index 9d1f20bd..aed9c202 100644 --- a/client/src/pages/RecipesPage.tsx +++ b/client/src/pages/RecipesPage.tsx @@ -1,20 +1,21 @@ import { WorkspacesOutlined } from "@mui/icons-material"; import { Box, + CircularProgress, List, ListItemButton, ListItemIcon, ListItemText, - Typography as Type, } from "@mui/material"; import { Flex } from "components/generic/Flex"; import { Scroll } from "components/generic/Scrollbars"; +import { useSnackbar } from "components/generic/Snackbar"; import { useViewTreeContext } from "components/inspector/ViewTree"; import { useWorkspace } from "hooks/useWorkspace"; -import { map, startCase, values } from "lodash"; +import { chain as _, entries, map } from "lodash"; import { Page } from "pages/Page"; -import { ReactNode } from "react"; import { useAsync } from "react-async-hook"; +import { useLoadingState } from "slices/loading"; function stripExtension(path: string) { return path.split(".")[0]; @@ -25,44 +26,40 @@ function basename(path: string) { } export function RecipesPage() { + const notify = useSnackbar(); const { controls, onChange, state } = useViewTreeContext(); const { load } = useWorkspace(); + const usingLoadingState = useLoadingState(); - const { result: files } = useAsync(async () => { + const { result: files, loading } = useAsync(async () => { const paths = import.meta.glob("/public/recipes/*.workspace", { as: "url", }); - return await Promise.all(values(paths).map((f) => f())); + return await Promise.all( + entries(paths).map((entry) => getFileInfo(...entry)) + ); }, []); - async function open(path: string) { - try { - const response = await fetch(path); - - if (!response.ok) { - throw new Error("Network response was not ok"); + const open = (path: string) => + usingLoadingState(async () => { + try { + notify(`Loading ${basename(path)}...`); + const response = await fetch(path); + if (!response.ok) { + notify(`Couldn't load ${basename(path)}`, `Network error`, { + error: true, + }); + } + const blob = await response.blob(); + const file = new File([blob], basename(path), { type: blob.type }); + // It is correct to not wait for this promise + load(file); + } catch (e) { + notify(`Couldn't load ${basename(path)}`, `${e}`, { + error: true, + }); } - - const blob = await response.blob(); - const filename = basename(path); - const file = new File([blob], filename, { type: blob.type }); - - load(file); - } catch (error) { - console.error("There was a problem with the fetch operation:", error); - } - } - - function renderSection(label: ReactNode, content: ReactNode) { - return ( - - - {label} - - {content} - - ); - } + }); return ( @@ -70,23 +67,19 @@ export function RecipesPage() { - {renderSection( - Recipes, - <> - - {map(files, (path, i) => ( - open(path)}> - - - - - - ))} - - + {!loading ? ( + + {map(files, ({ name, path }, i) => ( + open(path)}> + + + + + + ))} + + ) : ( + )} @@ -96,3 +89,9 @@ export function RecipesPage() { ); } +async function getFileInfo(k: string, f: () => Promise) { + return { + name: _(k).thru(basename).thru(stripExtension).startCase().value(), + path: await f(), + }; +} diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 99097abc..3521f28b 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -21,7 +21,6 @@ import { TreePage } from "./TreePage"; import { ViewportPage } from "./ViewportPage"; import { RecipesPage } from "./RecipesPage"; - export type PageMeta = { id: string; name: string; @@ -30,6 +29,12 @@ export type PageMeta = { }; export const pages: Dictionary = { + recipes: { + id: "recipes", + name: "Recipes", + icon: , + content: RecipesPage, + }, viewport: { id: "viewport", name: "Viewport", @@ -78,10 +83,4 @@ export const pages: Dictionary = { icon: , content: AboutPage, }, - recipes: { - id: "recipes", - name: "Recipes", - icon: , - content: RecipesPage, - }, }; diff --git a/client/src/slices/loading.ts b/client/src/slices/loading.ts index 6c11314b..0820bfbe 100644 --- a/client/src/slices/loading.ts +++ b/client/src/slices/loading.ts @@ -8,6 +8,7 @@ type Loading = { map: number; connections: number; features: number; + general: number; }; type A = { action: "start" | "end"; key: keyof Loading }; @@ -18,6 +19,7 @@ export const [useLoading, LoadingProvider] = createSlice( connections: 0, features: 0, map: 0, + general: 0, }, { reduce: (prev, { action, key }: A) => { @@ -40,7 +42,7 @@ export function useAnyLoading() { return some(values(loading)); } -export function useLoadingState(key: keyof Loading) { +export function useLoadingState(key: keyof Loading = "general") { const [, dispatch] = useLoading(); return useCallback(