WIP: feat: Create new db app #32

Closed
Ghost wants to merge 6 commits from feat/migrate-to-blitzjs into development
6 changed files with 348 additions and 201 deletions
Showing only changes of commit f90be5822b - Show all commits

View 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>
)
}

View 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>
)
}

View 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"
}
}
]}

View File

@ -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 {Box, Theme, Typography} from '@mui/material'
import {makeStyles} from "@mui/styles"; import {makeStyles} from "@mui/styles";
import React from "react"; import React from "react";
import { BlitzPage } from 'blitz'; import { BlitzPage } from 'blitz';
import { Layout } from 'app/core/layouts/Layout'; 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) => ({ const useStyles = makeStyles((theme: Theme) => ({
container: { container: {
@ -209,8 +14,14 @@ const useStyles = makeStyles((theme: Theme) => ({
flexGrow: 1, flexGrow: 1,
height: '100vh', height: '100vh',
maxheight: '100vh', maxheight: '100vh',
},
searchContainer: {
display: 'flex',
flexDirection: 'row',
flexGrow: 1,
padding: '2vh 2vw 1vh 2vw'
} }
})) }));
const Search: BlitzPage = () => { const Search: BlitzPage = () => {
const classes = useStyles(); const classes = useStyles();
@ -218,7 +29,10 @@ const Search: BlitzPage = () => {
return ( return (
<> <>
<Box className={classes.container}> <Box className={classes.container}>
<Typography>Searching</Typography> <NavigationBreadcrumb/>
<Box className={classes.searchContainer}>
<SearchArea/>
</Box>
</Box> </Box>
</> </>
) )

View File

@ -1,6 +1,7 @@
{ {
"name": "db", "name": "db",
"version": "1.0.0", "version": "1.0.0",
"type": "commonjs",
"scripts": { "scripts": {
"dev": "blitz dev", "dev": "blitz dev",
"build": "blitz build", "build": "blitz build",

View File

@ -17,7 +17,7 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"tsBuildInfoFile": ".tsbuildinfo" "tsBuildInfoFile": ".tsbuildinfo",
}, },
"exclude": ["node_modules", "**/*.e2e.ts", "cypress"], "exclude": ["node_modules", "**/*.e2e.ts", "cypress"],
"include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"] "include": ["blitz-env.d.ts", "**/*.ts", "**/*.tsx"]