diff --git a/db/app/core/components/NavigationBreadcrumb.tsx b/db/app/core/components/NavigationBreadcrumb.tsx new file mode 100644 index 0000000..452a3ec --- /dev/null +++ b/db/app/core/components/NavigationBreadcrumb.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react' +import { Box, Breadcrumbs, Link, Theme, Typography } from '@mui/material' +import { makeStyles } from '@mui/styles' +import { useEffect } from 'react' +import { useGlobalState } from '../state/GlobalState' +import { ItemWithLocale } from '../dto/ItemWithLocale' + +const useStyles = makeStyles((theme: Theme) => ({ + breadcrumbHolder: { + display: 'flex', + flex: '0 1 3vh', + flexDirection: 'row', + alignItems: 'center', + padding: '0 10vw 0 10vw', + borderBottom: `1px solid ${theme.palette.background.paper}`, + }, + breadcrumb: { + display: 'flex', + flex: '0 1 3vh', + flexDirection: 'row', + flexGrow: 1, + }, + link: { + color: theme.palette.text.secondary, + display: 'flex', + padding: '0 1vw 0 1vw', + height: '100%', + alignItems: 'center', + borderBottom: `1px solid transparent`, + '&:hover': { + color: theme.palette.action.hover, + cursor: 'pointer', + }, + }, + currentItem: { + cursor: 'default', + borderBottom: `1px solid ${theme.palette.action.hover}`, + }, +})) + +export const NavigationBreadcrumb = () => { + const classes = useStyles() + const setSelectedItem = useGlobalState((state) => state.setSelectedItem) + const itemHierachyState = useGlobalState((state) => state.itemsHierarchy) + const [searchInputState, setSearchInput] = useGlobalState((state) => [ + state.searchInput, + state.setSearchInput, + ]) + const selectedItem = useGlobalState((state) => state.selectedItem) + const [currentHierarchy, setCurrentHierarchy] = useState([]) + + useEffect(() => { + if (!selectedItem) return; + + const hierarchy: ItemWithLocale[] = [selectedItem] + let currItemID: string | undefined = selectedItem?.item?._parent + while (currItemID) { + const item: ItemWithLocale = itemHierachyState[currItemID]! + hierarchy.push(item) + currItemID = item?.item?._parent + } + setCurrentHierarchy(hierarchy.filter(elt => elt !== undefined && elt !== null).reverse()) + }, [selectedItem, itemHierachyState]) + + const formatLink = (item: ItemWithLocale, idx: string) => { + if ( + searchInputState === item.locale.Name || + searchInputState === item.item._id || + searchInputState === item.item._name + ) { + return ( + + {item.locale.Name ? item.locale.Name : item.item._name} + + ) + } else { + return ( + { + setSearchInput(item.item._id) + setSelectedItem(undefined) + }} + className={classes.link} + > + {item.locale.Name ? item.locale.Name : item.item._name} + + ) + } + } + + return ( + + + + Home + + {currentHierarchy.map((item, idx) => formatLink(item, idx.toString()))} + + + ) +} diff --git a/db/app/core/components/SearchArea.tsx b/db/app/core/components/SearchArea.tsx new file mode 100644 index 0000000..0b98af9 --- /dev/null +++ b/db/app/core/components/SearchArea.tsx @@ -0,0 +1,207 @@ +import {ReactElement, SyntheticEvent, useCallback, useEffect, useState} from 'react' +import {Autocomplete, Box, CircularProgress, Theme, Typography,} from '@mui/material' +import {makeStyles} from '@mui/styles' +import TextField from '@mui/material/TextField' +import {getItem, getItemHierarchy, searchItem} from '../dataaccess/ItemBackend'; +import {ItemOption} from '../dto/ItemOption' +import {useGlobalState} from '../state/GlobalState' +// import {useNavigate, useParams} from "react-router-dom"; +import {useRouter, dynamic } from "blitz"; + +let ReactJson +if (process.browser){ + ReactJson = dynamic(() => import("react-json-view")) +} + +interface IItemOption { + id: string + name: string + shortName?: string +} + +const useStyles = makeStyles((theme: Theme) => ({ + searchAreaHolder: { + display: 'flex', + flexGrow: 1, + flexDirection: 'column', + background: theme.palette.background.paper, + padding: '2vh 2vw 2vh 2vw', + }, + jsonHolder: { + display: 'flex', + flexGrow: 1, + alignItems: 'center', + flexDirection: 'column', + background: theme.palette.background.paper, + maxHeight: '80vh', + }, + autocomplete: {}, +})) + +export const SearchArea = () => { + const classes = useStyles(); + const router = useRouter() + const params = router.query; + const preferedLocale = useGlobalState((state) => state.preferedLocale) + const preferedJsonViewerTheme = useGlobalState( + useCallback((state) => state.preferedJsonViewerTheme, []), + ) + const [searchInputState, setSearchInput] = useGlobalState((state) => [ + state.searchInput, + state.setSearchInput, + ]) + const [setHierarchy, initHierarchy] = useGlobalState((state) => [state.setHierarchy, state.initHierarchy]) + const [selectedItem, setSelectedItem] = useGlobalState((state) => [ + state.selectedItem, + state.setSelectedItem, + ]) + const [selectOptions, setSelecteOptions] = useState([]) + const [isbusy, setIsBusy] = useState(false) + const searchThreshold = 3 + + const handleNameInput = useCallback(async (input: string) => { + const searchResults = await searchItem(input, preferedLocale) + const options = searchResults?.map((res) => ({ + id: res.item._id, + name: res.locale.Name ? res.locale.Name : res.item._name, + shortName: JSON.stringify(res.locale.ShortName) + })) + setSelecteOptions(options ? options : []) + }, [preferedLocale]) + + const handleIDInput = useCallback(async (input: string) => { + const itemJson = await getItem(input, preferedLocale) + if (!itemJson) { + setSelectedItem(undefined) + setSearchInput('') + return; + } + + setSelectedItem(itemJson) + const itemObj = { + id: itemJson.item._id, + name: itemJson.locale.Name ? itemJson.locale.Name : itemJson.item._name, + shortName: itemJson.locale.ShortName + } + setSelecteOptions([itemObj]) + setSearchInput(itemObj.name) + + // Update hierachy + const itemHierarchy = await getItemHierarchy( + itemJson.item, + preferedLocale, + ) + setHierarchy(itemHierarchy ? itemHierarchy : {}) + // eslint-disable-next-line + }, []) // Need to only be created on startup + + useEffect(() => initHierarchy(), [initHierarchy]) + + useEffect(()=>{ + if (selectedItem){ + router.replace(`/search/${selectedItem.item._id}`) + } + },[selectedItem, router]) + + useEffect(() => { + if (searchInputState && searchInputState.match(/([a-z0-9]{24})/)) { + handleIDInput(searchInputState) + } + }, [handleIDInput, searchInputState]) + + const handleInput = useCallback(async (input: string) => { + if (!input || input.length < searchThreshold || isbusy) { + setSelectedItem(undefined) + setSelecteOptions([]) + setIsBusy(false) + return + } + setIsBusy(true) + + if (input.match(/([a-z0-9]{24})/)) await handleIDInput(input) + else await handleNameInput(input) + + setIsBusy(false) + }, [handleIDInput, handleNameInput, isbusy, setSelectedItem]) + + useEffect(() => { + if (!searchInputState && params.id) { + const newId = (params.id as string).trim(); + console.log(newId); + setSearchInput(newId); + (async () => await handleInput(newId))(); + } + }, [params, searchInputState, setSearchInput, handleInput, router]) + + const formatDisplayItems = () => { + // If loading + if (isbusy) return + + // If finished loading + console.log(process.browser) + if (selectedItem && ReactJson !== undefined) { + return () + } else return No data to display + // return No data to display + } + + const findOptionValue = (option: ItemOption, value: ItemOption): boolean => { + return option.name?.toLocaleLowerCase() === value.name?.toLocaleLowerCase() + || option.id?.toLocaleLowerCase() === value.id?.toLocaleLowerCase() + || option.shortName?.toLocaleLowerCase() === value.shortName?.toLocaleLowerCase() + } + + return ( + + ({name: elt.name, shortName: elt.shortName, id: elt.id}))} + getOptionLabel={(option) => (option.name ? option.name : option.id) as string} + isOptionEqualToValue={(option, value) => findOptionValue(option, value)} + open={!isbusy && searchInputState.length >= searchThreshold && (searchInputState !== selectedItem?.locale.Name && searchInputState !== selectedItem?.item._name)} + className={classes.autocomplete} + inputValue={searchInputState ? searchInputState : ''} + onInputChange={async (evt: SyntheticEvent, newValue: string) => { + if (!evt) return + setSelectedItem(undefined) + setSearchInput(newValue) + await handleInput(newValue.trim()) + }} + value={(() => { + const selectedOption = selectOptions.find(elt => elt.id === searchInputState || elt.name === searchInputState); + return selectedOption ? selectedOption : null; + })()} + onChange={async (event: SyntheticEvent, newValue: IItemOption | null) => { + if (newValue) { + const selectedOption = selectOptions.find( + (elt) => elt.name === newValue.name, + ) + if (selectedOption) await handleIDInput(selectedOption.id) + } + }} + renderInput={(params) => ( + + )} + renderOption={(props, option ) => ( +
  • {option.name}
  • + )} + filterOptions={(options, state) => options.filter(elt => { + return (elt.name?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase()) + || elt.id?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase()) + || elt.shortName?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())) + })} + filterSelectedOptions + /> + {formatDisplayItems()} +
    + ) +} diff --git a/db/app/core/dataaccess/ItemBackend.ts b/db/app/core/dataaccess/ItemBackend.ts new file mode 100644 index 0000000..78cc07a --- /dev/null +++ b/db/app/core/dataaccess/ItemBackend.ts @@ -0,0 +1,14 @@ +export const getItem = (test:any,test2:any):any=>{return null} +export const getItemHierarchy = (test:any,test2:any):any =>{return null} +export const searchItem = (test:any,test2:any):any[] =>{ return [ + { + item:{ + _id: 'ABCDE', + _name: 'Name', + }, + locale:{ + name: "test" + } + + } +]} \ No newline at end of file diff --git a/db/app/pages/index.tsx b/db/app/pages/index.tsx index 002d184..3c8010a 100644 --- a/db/app/pages/index.tsx +++ b/db/app/pages/index.tsx @@ -1,205 +1,10 @@ -// import { Image, BlitzPage } from "blitz" -// import logo from "public/logo.png" - -// /* -// * This file is just for a pleasant getting started page for your new app. -// * You can delete everything in here and start from scratch if you like. -// */ - -// const Home: BlitzPage = () => { -// return ( -//
    -//
    -//
    -// blitzjs -//
    -//

    -// Congrats! Your app is ready. -//

    -// -//
    - -// - -// -//
    -// ) -// } - -// Home.suppressFirstRenderFlicker = true - -// export default Home - - import {Box, Theme, Typography} from '@mui/material' import {makeStyles} from "@mui/styles"; import React from "react"; import { BlitzPage } from 'blitz'; import { Layout } from 'app/core/layouts/Layout'; +import { NavigationBreadcrumb } from 'app/core/components/NavigationBreadcrumb'; +import { SearchArea } from 'app/core/components/SearchArea'; const useStyles = makeStyles((theme: Theme) => ({ container: { @@ -209,8 +14,14 @@ const useStyles = makeStyles((theme: Theme) => ({ flexGrow: 1, height: '100vh', maxheight: '100vh', + }, + searchContainer: { + display: 'flex', + flexDirection: 'row', + flexGrow: 1, + padding: '2vh 2vw 1vh 2vw' } -})) +})); const Search: BlitzPage = () => { const classes = useStyles(); @@ -218,7 +29,10 @@ const Search: BlitzPage = () => { return ( <> - Searching + + + + ) diff --git a/db/package.json b/db/package.json index a74392f..45126a7 100644 --- a/db/package.json +++ b/db/package.json @@ -1,6 +1,7 @@ { "name": "db", "version": "1.0.0", + "type": "commonjs", "scripts": { "dev": "blitz dev", "build": "blitz build", @@ -40,4 +41,4 @@ "typescript": "~4.5" }, "private": true -} +} \ No newline at end of file diff --git a/db/tsconfig.json b/db/tsconfig.json index 8889c01..6678480 100644 --- a/db/tsconfig.json +++ b/db/tsconfig.json @@ -17,7 +17,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, - "tsBuildInfoFile": ".tsbuildinfo" + "tsBuildInfoFile": ".tsbuildinfo", }, "exclude": ["node_modules", "**/*.e2e.ts", "cypress"], "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"]