Add web interface and tests

This commit is contained in:
aet
2021-11-05 03:17:18 -04:00
parent 7f41a50fb1
commit fe926be0a6
66 changed files with 3390 additions and 1275 deletions

View File

@ -0,0 +1,142 @@
import styled from "@emotion/styled"
import { useCallback, useEffect, useMemo, useState } from "react"
import type { Vault, Item } from "opvault.js"
import { Category } from "opvault.js"
import { IoSearch } from "react-icons/io5"
import { ItemList } from "../components/ItemList"
import { ItemView } from "../components/Item"
import { reactIconClass } from "../components/CategoryIcon"
const Container = styled.div`
display: flex;
height: calc(100vh - var(--titlebar-height));
`
const ListContainer = styled.div`
width: 300px;
margin-right: 10px;
overflow-y: scroll;
overflow-x: hidden;
@media (prefers-color-scheme: dark) {
background: #202020;
}
`
const ItemContainer = styled.div`
width: calc(100% - 300px);
overflow: hidden;
`
const SearchContainer = styled.div`
text-align: center;
margin: 10px 0;
position: relative;
`
const SortContainer = styled.div`
margin: 10px 10px;
`
const SearchInput = styled.input`
--margin: 10px;
width: calc(100% - var(--margin) * 2);
margin: 0 var(--margin);
padding-left: 2em !important;
`
const SearchIcon = styled(IoSearch)`
position: absolute;
top: 9px;
left: 20px;
`
const enum SortBy {
Name,
CreatedAt,
UpdatedAt,
}
export const VaultView: React.FC<{ vault: Vault; onLock(): void }> = ({
vault,
onLock,
}) => {
const [items, setItems] = useState<Item[]>(() => [])
const [item, setItem] = useState<Item>()
const [sortBy, setSortBy] = useState(SortBy.Name)
const [search, setSearch] = useState("")
const compareFn = useMemo((): ((a: Item, b: Item) => number) => {
switch (sortBy) {
case SortBy.Name:
return (a, b) => (a.overview.title ?? "").localeCompare(b?.overview.title ?? "")
case SortBy.CreatedAt:
return (a, b) => a.createdAt - b.createdAt
case SortBy.UpdatedAt:
return (a, b) => a.updatedAt - b.updatedAt
}
}, [sortBy])
const sortedItem = useMemo(() => items.slice().sort(compareFn), [items, compareFn])
useEffect(() => {
setItem(undefined)
arrayFrom(vault.values()).then(setItems)
}, [vault])
const filtered = useMemo(
() =>
sortedItem
.filter(x => x.category !== Category.Tombstone)
.filter(
search
? x =>
stringCompare(search, x.overview.title) ||
stringCompare(search, x.overview.ainfo)
: () => true
),
[sortedItem, search]
)
return (
<Container>
<ListContainer>
<div
style={{
margin: "10px 10px",
}}
>
<button onClick={onLock}>Lock</button>
</div>
<SearchContainer>
<SearchInput
type="search"
value={search}
onChange={e => setSearch(e.currentTarget.value)}
/>
<SearchIcon className={reactIconClass} />
</SearchContainer>
<SortContainer>
<select
style={{ width: "100%" }}
value={sortBy}
onChange={e => setSortBy(+e.currentTarget.value)}
>
<option value={SortBy.Name}>Sort by Name</option>
<option value={SortBy.CreatedAt}>Sort by Created Time</option>
<option value={SortBy.UpdatedAt}>Sort by Updated Time</option>
</select>
</SortContainer>
<ItemList items={filtered} onSelect={setItem} selected={item} />
</ListContainer>
<ItemContainer>{item && <ItemView item={item} />}</ItemContainer>
</Container>
)
}
async function arrayFrom<T>(generator: AsyncGenerator<T, void, unknown>) {
const list: T[] = []
for await (const value of generator) {
list.push(value)
}
return list
}
function stringCompare(search: string, source?: string) {
if (!search) return true
if (!source) return false
return source.toLocaleLowerCase().includes(search.toLocaleLowerCase())
}