WIP: feat: Create new db app #32
111
db/app/core/components/NavigationBreadcrumb.tsx
Normal file
111
db/app/core/components/NavigationBreadcrumb.tsx
Normal file
@ -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<ItemWithLocale[]>([])
|
||||
|
||||
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 (
|
||||
<Typography key={item.item._id} variant="body2" className={classes.currentItem}>
|
||||
{item.locale.Name ? item.locale.Name : item.item._name}
|
||||
</Typography>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Link
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setSearchInput(item.item._id)
|
||||
setSelectedItem(undefined)
|
||||
}}
|
||||
className={classes.link}
|
||||
>
|
||||
<Typography variant="body2">{item.locale.Name ? item.locale.Name : item.item._name}</Typography>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className={classes.breadcrumbHolder}>
|
||||
<Breadcrumbs aria-label="breadcrumb" className={classes.breadcrumb} id='navigation-breadcrumb'>
|
||||
<Link
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
key={'home'}
|
||||
href="/"
|
||||
id='home-breadcrumb'
|
||||
className={classes.link}
|
||||
>
|
||||
<Typography variant="body2">Home</Typography>
|
||||
</Link>
|
||||
{currentHierarchy.map((item, idx) => formatLink(item, idx.toString()))}
|
||||
</Breadcrumbs>
|
||||
</Box>
|
||||
)
|
||||
}
|
207
db/app/core/components/SearchArea.tsx
Normal file
207
db/app/core/components/SearchArea.tsx
Normal file
@ -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<ItemOption[]>([])
|
||||
const [isbusy, setIsBusy] = useState<boolean>(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 <CircularProgress size={100}/>
|
||||
|
||||
// If finished loading
|
||||
console.log(process.browser)
|
||||
if (selectedItem && ReactJson !== undefined) {
|
||||
return (<ReactJson
|
||||
src={selectedItem!}
|
||||
theme={preferedJsonViewerTheme}
|
||||
style={{
|
||||
marginTop: '2vh',
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
display: 'flex',
|
||||
}}
|
||||
/>)
|
||||
} else return <Typography id='search-no-data'>No data to display</Typography>
|
||||
// return <Typography id='search-no-data'>No data to display</Typography>
|
||||
}
|
||||
|
||||
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 (
|
||||
<Box className={classes.searchAreaHolder}>
|
||||
<Autocomplete
|
||||
id='search-autocomplete'
|
||||
options={selectOptions.map((elt) => ({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) => (
|
||||
<TextField {...params} label="Search by name or ID"/>
|
||||
)}
|
||||
renderOption={(props, option ) => (
|
||||
<li {...props} key={option.id}><Typography>{option.name}</Typography></li>
|
||||
)}
|
||||
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
|
||||
/>
|
||||
<Box className={classes.jsonHolder}>{formatDisplayItems()}</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
14
db/app/core/dataaccess/ItemBackend.ts
Normal file
14
db/app/core/dataaccess/ItemBackend.ts
Normal file
@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
]}
|
@ -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 (
|
||||
// <div className="container">
|
||||
// <main>
|
||||
// <div className="logo">
|
||||
// <Image src={logo} alt="blitzjs" />
|
||||
// </div>
|
||||
// <p>
|
||||
// <strong>Congrats!</strong> Your app is ready.
|
||||
// </p>
|
||||
// <div className="buttons" style={{ marginTop: "5rem" }}>
|
||||
// <a
|
||||
// className="button"
|
||||
// href="https://blitzjs.com/docs/getting-started?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// >
|
||||
// Documentation
|
||||
// </a>
|
||||
// <a
|
||||
// className="button-outline"
|
||||
// href="https://github.com/blitz-js/blitz"
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// >
|
||||
// Github Repo
|
||||
// </a>
|
||||
// <a
|
||||
// className="button-outline"
|
||||
// href="https://discord.blitzjs.com"
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// >
|
||||
// Discord Community
|
||||
// </a>
|
||||
// </div>
|
||||
// </main>
|
||||
|
||||
// <footer>
|
||||
// <a
|
||||
// href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer"
|
||||
// >
|
||||
// Powered by Blitz.js
|
||||
// </a>
|
||||
// </footer>
|
||||
|
||||
// <style jsx global>{`
|
||||
// @import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;700&display=swap");
|
||||
|
||||
// html,
|
||||
// body {
|
||||
// padding: 0;
|
||||
// margin: 0;
|
||||
// font-family: "Libre Franklin", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
// Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
// }
|
||||
|
||||
// * {
|
||||
// -webkit-font-smoothing: antialiased;
|
||||
// -moz-osx-font-smoothing: grayscale;
|
||||
// box-sizing: border-box;
|
||||
// }
|
||||
// .container {
|
||||
// min-height: 100vh;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// }
|
||||
|
||||
// main {
|
||||
// padding: 5rem 0;
|
||||
// flex: 1;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// }
|
||||
|
||||
// main p {
|
||||
// font-size: 1.2rem;
|
||||
// }
|
||||
|
||||
// p {
|
||||
// text-align: center;
|
||||
// }
|
||||
|
||||
// footer {
|
||||
// width: 100%;
|
||||
// height: 60px;
|
||||
// border-top: 1px solid #eaeaea;
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// background-color: #45009d;
|
||||
// }
|
||||
|
||||
// footer a {
|
||||
// display: flex;
|
||||
// justify-content: center;
|
||||
// align-items: center;
|
||||
// }
|
||||
|
||||
// footer a {
|
||||
// color: #f4f4f4;
|
||||
// text-decoration: none;
|
||||
// }
|
||||
|
||||
// .logo {
|
||||
// margin-bottom: 2rem;
|
||||
// }
|
||||
|
||||
// .logo img {
|
||||
// width: 300px;
|
||||
// }
|
||||
|
||||
// .buttons {
|
||||
// display: grid;
|
||||
// grid-auto-flow: column;
|
||||
// grid-gap: 0.5rem;
|
||||
// }
|
||||
// .button {
|
||||
// font-size: 1rem;
|
||||
// background-color: #6700eb;
|
||||
// padding: 1rem 2rem;
|
||||
// color: #f4f4f4;
|
||||
// text-align: center;
|
||||
// }
|
||||
|
||||
// .button.small {
|
||||
// padding: 0.5rem 1rem;
|
||||
// }
|
||||
|
||||
// .button:hover {
|
||||
// background-color: #45009d;
|
||||
// }
|
||||
|
||||
// .button-outline {
|
||||
// border: 2px solid #6700eb;
|
||||
// padding: 1rem 2rem;
|
||||
// color: #6700eb;
|
||||
// text-align: center;
|
||||
// }
|
||||
|
||||
// .button-outline:hover {
|
||||
// border-color: #45009d;
|
||||
// color: #45009d;
|
||||
// }
|
||||
|
||||
// pre {
|
||||
// background: #fafafa;
|
||||
// border-radius: 5px;
|
||||
// padding: 0.75rem;
|
||||
// text-align: center;
|
||||
// }
|
||||
// code {
|
||||
// font-size: 0.9rem;
|
||||
// font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
// Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
// }
|
||||
|
||||
// .grid {
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// flex-wrap: wrap;
|
||||
|
||||
// max-width: 800px;
|
||||
// margin-top: 3rem;
|
||||
// }
|
||||
|
||||
// @media (max-width: 600px) {
|
||||
// .grid {
|
||||
// width: 100%;
|
||||
// flex-direction: column;
|
||||
// }
|
||||
// }
|
||||
// `}</style>
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<Box className={classes.container}>
|
||||
<Typography>Searching</Typography>
|
||||
<NavigationBreadcrumb/>
|
||||
<Box className={classes.searchContainer}>
|
||||
<SearchArea/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user