fix types
This commit is contained in:
parent
504b47335d
commit
7fdbef7573
@ -16,8 +16,8 @@ knex.destroy()
|
|||||||
|
|
||||||
async function readdir(dir: string) {
|
async function readdir(dir: string) {
|
||||||
const files = (await fs.readdir(dir)).toSorted((a: string, b: string) => {
|
const files = (await fs.readdir(dir)).toSorted((a: string, b: string) => {
|
||||||
const [, aNum] = a.match(rFileName)
|
const [, aNum] = a.match(rFileName) as string[]
|
||||||
const [, bNum] = b.match(rFileName)
|
const [, bNum] = b.match(rFileName) as string[]
|
||||||
|
|
||||||
if (parseInt(aNum) > parseInt(bNum)) {
|
if (parseInt(aNum) > parseInt(bNum)) {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
// import s from './accounts_page.module.scss'
|
|
||||||
|
|
||||||
const AccountsPage = () => {
|
import type { Account } from '../../../shared/types.ts'
|
||||||
const [accounts, setAccounts] = useState([])
|
|
||||||
|
const AccountsPage: FunctionComponent = () => {
|
||||||
|
const [accounts, setAccounts] = useState<Account[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek('/api/accounts').then((accounts) => {
|
rek('/api/accounts').then(setAccounts)
|
||||||
setAccounts(accounts)
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useCallback, useEffect } from 'preact/hooks'
|
import { useCallback, useEffect } from 'preact/hooks'
|
||||||
import { LocationProvider, Route, Router } from 'preact-iso'
|
import { LocationProvider, Route, Router } from 'preact-iso'
|
||||||
import { get } from 'lowline'
|
import { get } from 'lowline'
|
||||||
@ -11,6 +11,11 @@ import throttle from '../../shared/utils/throttle.ts'
|
|||||||
|
|
||||||
import s from './app.module.scss'
|
import s from './app.module.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
error?: Error
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
const remember = throttle(function remember() {
|
const remember = throttle(function remember() {
|
||||||
window.history.replaceState(
|
window.history.replaceState(
|
||||||
{
|
{
|
||||||
@ -22,7 +27,7 @@ const remember = throttle(function remember() {
|
|||||||
)
|
)
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
export default function App({ error, title }) {
|
const App: FunctionComponent<Props> = ({ error, title }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addEventListener('scroll', remember)
|
addEventListener('scroll', remember)
|
||||||
|
|
||||||
@ -52,7 +57,7 @@ export default function App({ error, title }) {
|
|||||||
) : (
|
) : (
|
||||||
<Router onRouteChange={onRouteChange}>
|
<Router onRouteChange={onRouteChange}>
|
||||||
{routes.map((route) => (
|
{routes.map((route) => (
|
||||||
<Route key={route.path} route={route} path={route.path} component={route.component} />
|
<Route key={route.path} path={route.path} component={route.component} />
|
||||||
))}
|
))}
|
||||||
</Router>
|
</Router>
|
||||||
)}
|
)}
|
||||||
@ -63,3 +68,5 @@ export default function App({ error, title }) {
|
|||||||
</LocationProvider>
|
</LocationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
@ -6,13 +6,15 @@ import Head from './head.ts'
|
|||||||
import { formatNumber } from '../utils/format_number.ts'
|
import { formatNumber } from '../utils/format_number.ts'
|
||||||
import s from './balances_page.module.scss'
|
import s from './balances_page.module.scss'
|
||||||
|
|
||||||
const BalancesPage = () => {
|
import type { Balance, FinancialYear } from '../../../shared/types.ts'
|
||||||
const [balances, setBalances] = useState([])
|
|
||||||
|
const BalancesPage: FunctionComponent = () => {
|
||||||
|
const [balances, setBalances] = useState<Balance[]>([])
|
||||||
const [years, setYears] = useState<number[]>([])
|
const [years, setYears] = useState<number[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/balances`).then(setBalances)
|
rek(`/api/balances`).then(setBalances)
|
||||||
rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year)))
|
rek(`/api/financial-years`).then((financialYears: FinancialYear[]) => setYears(financialYears.map((fy) => fy.year)))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||||
import { useLocation } from 'preact-iso'
|
import { useLocation } from 'preact-iso'
|
||||||
import { isEmpty } from 'lowline'
|
import { isEmpty } from 'lowline'
|
||||||
@ -8,12 +8,14 @@ import rek from 'rek'
|
|||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
import serializeForm from '../../shared/utils/serialize_form.ts'
|
import serializeForm from '../../shared/utils/serialize_form.ts'
|
||||||
|
|
||||||
|
import type { Entry, FinancialYear, Journal } from '../../../shared/types.ts'
|
||||||
|
|
||||||
const dateYear = new Date().getFullYear()
|
const dateYear = new Date().getFullYear()
|
||||||
|
|
||||||
const EntriesPage = () => {
|
const EntriesPage: FunctionComponent = () => {
|
||||||
const [journals, setJournals] = useState([])
|
const [journals, setJournals] = useState<Journal[]>([])
|
||||||
const [financialYears, setFinancialYears] = useState([])
|
const [financialYears, setFinancialYears] = useState<FinancialYear[]>([])
|
||||||
const [entries, setEntries] = useState([])
|
const [entries, setEntries] = useState<Entry[]>([])
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
@ -22,7 +24,7 @@ const EntriesPage = () => {
|
|||||||
const onSubmit = useCallback((e: SubmitEvent) => {
|
const onSubmit = useCallback((e: SubmitEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const values = serializeForm(e.target as HTMLFormElement)
|
const values = serializeForm(e.target as HTMLFormElement) as Record<string, string>
|
||||||
|
|
||||||
const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
|
const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
|
||||||
|
|
||||||
@ -30,16 +32,12 @@ const EntriesPage = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek('/api/journals').then((journals) => {
|
rek('/api/journals').then(setJournals)
|
||||||
setJournals(journals)
|
rek('/api/financial-years').then(setFinancialYears)
|
||||||
})
|
|
||||||
rek('/api/financial-years').then((financialYears) => {
|
|
||||||
setFinancialYears(financialYears)
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/entries?journal=${selectedJournal}&year=${selectedYear}`).then((entries) => setEntries(entries))
|
rek(`/api/entries?journal=${selectedJournal}&year=${selectedYear}`).then(setEntries)
|
||||||
}, [selectedJournal, selectedYear])
|
}, [selectedJournal, selectedYear])
|
||||||
|
|
||||||
return financialYears.length && journals.length ? (
|
return financialYears.length && journals.length ? (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { useRoute } from 'preact-iso'
|
import { useRoute } from 'preact-iso'
|
||||||
import { formatNumber } from '../utils/format_number.ts'
|
import { formatNumber } from '../utils/format_number.ts'
|
||||||
@ -8,14 +8,12 @@ import { type Entry } from '../../../shared/types.ts'
|
|||||||
|
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
|
|
||||||
const EntriesPage = () => {
|
const EntryPage: FunctionComponent = () => {
|
||||||
const [entry, setEntry] = useState<Entry>(null)
|
const [entry, setEntry] = useState<Entry | null>(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/entries/${route.params.id}`).then((entry) => {
|
rek(`/api/entries/${route.params.id}`).then(setEntry)
|
||||||
setEntry(entry)
|
|
||||||
})
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!entry) return
|
if (!entry) return
|
||||||
@ -84,4 +82,4 @@ const EntriesPage = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EntriesPage
|
export default EntryPage
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
|
|
||||||
const OtherPage = ({ error }) => (
|
const OtherPage: FunctionComponent<{ error: Error }> = ({ error }) => (
|
||||||
<section>
|
<section>
|
||||||
<Head>
|
<Head>
|
||||||
<title> : Error</title>
|
<title> : Error</title>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
|
||||||
const Footer = () => <footer />
|
const Footer: FunctionComponent = () => <footer />
|
||||||
|
|
||||||
export default Footer
|
export default Footer
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { toChildArray, Component } from 'preact'
|
import { toChildArray, Component, type VNode } from 'preact'
|
||||||
import { mapKeys } from 'lowline'
|
import { mapKeys } from 'lowline'
|
||||||
|
|
||||||
|
type Tag = {
|
||||||
|
type: string
|
||||||
|
attributes: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
const CLASSNAME = '__preact_generated__'
|
const CLASSNAME = '__preact_generated__'
|
||||||
|
|
||||||
const DOMAttributeNames = {
|
const DOMAttributeNames: Record<string, string> = {
|
||||||
acceptCharset: 'accept-charset',
|
acceptCharset: 'accept-charset',
|
||||||
className: 'class',
|
className: 'class',
|
||||||
htmlFor: 'for',
|
htmlFor: 'for',
|
||||||
@ -12,11 +17,11 @@ const DOMAttributeNames = {
|
|||||||
|
|
||||||
const isBrowser = typeof window !== 'undefined'
|
const isBrowser = typeof window !== 'undefined'
|
||||||
|
|
||||||
let mounted = []
|
let mounted: Head[] = []
|
||||||
|
|
||||||
function reducer(components) {
|
function reducer(components: Head[]) {
|
||||||
return components
|
return components
|
||||||
.map((c) => toChildArray(c.props.children))
|
.map((c) => toChildArray(c.props.children) as VNode<any>[])
|
||||||
.reduce((result, c) => result.concat(c), [])
|
.reduce((result, c) => result.concat(c), [])
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(unique())
|
.filter(unique())
|
||||||
@ -27,18 +32,18 @@ function reducer(components) {
|
|||||||
result.title += toChildArray(c.props.children).join('')
|
result.title += toChildArray(c.props.children).join('')
|
||||||
} else {
|
} else {
|
||||||
result.tags.push({
|
result.tags.push({
|
||||||
type: c.type,
|
type: c.type as string,
|
||||||
attributes: mapKeys(c.props, (_value, key) => DOMAttributeNames[key] || key),
|
attributes: mapKeys(c.props, (_value, key) => DOMAttributeNames[key] || key),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
},
|
},
|
||||||
{ title: '', tags: [] },
|
{ title: '', tags: [] } as { title: string; tags: Tag[] },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateClient({ title, tags }) {
|
function updateClient({ title, tags }: { title: string; tags: Tag[] }) {
|
||||||
const head = document.head
|
const head = document.head
|
||||||
|
|
||||||
const prevElements = Array.from(head.getElementsByClassName(CLASSNAME))
|
const prevElements = Array.from(head.getElementsByClassName(CLASSNAME))
|
||||||
@ -60,7 +65,7 @@ function updateClient({ title, tags }) {
|
|||||||
document.title = title
|
document.title = title
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDOMElement(tag) {
|
function createDOMElement(tag: Tag) {
|
||||||
const el = document.createElement(tag.type)
|
const el = document.createElement(tag.type)
|
||||||
const attributes = tag.attributes || {}
|
const attributes = tag.attributes || {}
|
||||||
|
|
||||||
@ -80,11 +85,11 @@ const METATYPES = ['name', 'httpEquiv', 'charSet', 'itemProp']
|
|||||||
// returns a function for filtering head child elements
|
// returns a function for filtering head child elements
|
||||||
// which shouldn't be duplicated, like <title/>.
|
// which shouldn't be duplicated, like <title/>.
|
||||||
function unique() {
|
function unique() {
|
||||||
const tags = []
|
const tags: string[] = []
|
||||||
const metaTypes = []
|
const metaTypes: string[] = []
|
||||||
const metaCategories = {}
|
const metaCategories: Record<string, string[]> = {}
|
||||||
|
|
||||||
return (h) => {
|
return (h: VNode<Record<string, string>>) => {
|
||||||
switch (h.type) {
|
switch (h.type) {
|
||||||
case 'base':
|
case 'base':
|
||||||
if (~tags.indexOf(h.type)) {
|
if (~tags.indexOf(h.type)) {
|
||||||
@ -109,8 +114,8 @@ function unique() {
|
|||||||
|
|
||||||
metaTypes.push(metatype)
|
metaTypes.push(metatype)
|
||||||
} else {
|
} else {
|
||||||
const category = h.props[metatype]
|
const category = h.props[metatype] as string
|
||||||
const categories = metaCategories[metatype] || []
|
const categories: string[] = metaCategories[metatype] || []
|
||||||
|
|
||||||
if (~categories.indexOf(category)) {
|
if (~categories.indexOf(category)) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import s from './header.module.scss'
|
import s from './header.module.scss'
|
||||||
|
|
||||||
const Header = ({ routes }) => (
|
import type { Route } from '../../../shared/types.ts'
|
||||||
|
|
||||||
|
const Header: FunctionComponent<{ routes: Route[] }> = ({ routes }) => (
|
||||||
<header>
|
<header>
|
||||||
<h1>BRF Tegeltrasten</h1>
|
<h1>BRF Tegeltrasten</h1>
|
||||||
<nav className={s.nav}>
|
<nav className={s.nav}>
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { h, type FunctionalComponent } from 'preact'
|
|||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
|
|
||||||
|
import type { Transaction } from '../../../shared/types.ts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
year: number
|
year: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Invoices: FunctionalComponent<Props> = ({ year }) => {
|
const Invoices: FunctionalComponent<Props> = ({ year }) => {
|
||||||
const [invoices, setInvoices] = useState([])
|
const [invoices, setInvoices] = useState<Transaction[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/invoices/by-year/${year}`).then(setInvoices)
|
rek(`/api/invoices/by-year/${year}`).then(setInvoices)
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { useRoute } from 'preact-iso'
|
import { useRoute } from 'preact-iso'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
import { formatNumber } from '../utils/format_number.ts'
|
import { formatNumber } from '../utils/format_number.ts'
|
||||||
|
|
||||||
const InvoicePage = () => {
|
import type { Invoice } from '../../../shared/types.ts'
|
||||||
const [invoice, setInvoice] = useState(null)
|
|
||||||
|
const InvoicePage: FunctionComponent = () => {
|
||||||
|
const [invoice, setInvoice] = useState<Invoice | null>(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/invoices/${route.params.id}`).then((invoice) => setInvoice(invoice))
|
rek(`/api/invoices/${route.params.id}`).then(setInvoice)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,25 +1,25 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
// @ts-ignore
|
||||||
import Format from 'easy-tz/format'
|
import Format from 'easy-tz/format'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import { useRoute } from 'preact-iso'
|
import { useRoute } from 'preact-iso'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
|
import type { Invoice, Supplier } from '../../../shared/types.ts'
|
||||||
|
|
||||||
const format = Format.bind(null, null, 'YYYY.MM.DD')
|
const format = Format.bind(null, null, 'YYYY.MM.DD')
|
||||||
|
|
||||||
const InvoicesPage = () => {
|
const InvoicesPage: FunctionComponent = () => {
|
||||||
const [supplier, setSupplier] = useState(null)
|
const [supplier, setSupplier] = useState<Supplier | null>(null)
|
||||||
const [invoices, setInvoices] = useState([])
|
const [invoices, setInvoices] = useState<Invoice[]>([])
|
||||||
const [totalAmount, setTotalAmount] = useState<number>(null)
|
const [totalAmount, setTotalAmount] = useState<number | null>(null)
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/suppliers/${route.params.supplier}`).then((supplier) => setSupplier(supplier))
|
rek(`/api/suppliers/${route.params.supplier}`).then(setSupplier)
|
||||||
rek(`/api/invoices?supplier=${route.params.supplier}`).then((invoices) => setInvoices(invoices))
|
rek(`/api/invoices?supplier=${route.params.supplier}`).then(setInvoices)
|
||||||
rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`).then((totalAmount) =>
|
rek(`/api/invoices/total-amount?supplier=${route.params.supplier}`).then(setTotalAmount)
|
||||||
setTotalAmount(totalAmount.amount),
|
|
||||||
)
|
|
||||||
}, [route.params.supplier])
|
}, [route.params.supplier])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
|
|
||||||
const InvoicesPage = () => {
|
import type { Supplier } from '../../../shared/types.ts'
|
||||||
const [suppliers, setSuppliers] = useState([])
|
|
||||||
|
const InvoicesPage: FunctionComponent = () => {
|
||||||
|
const [suppliers, setSuppliers] = useState<Supplier[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek('/api/suppliers').then((suppliers) => setSuppliers(suppliers))
|
rek('/api/suppliers').then(setSuppliers)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
|
||||||
export default function NotFoundPage() {
|
const NotFoundPage: FunctionComponent = () => {
|
||||||
return <h1>Not Found</h1>
|
return <h1>Not Found</h1>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default NotFoundPage
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { h, type FunctionalComponent } from 'preact'
|
|||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
|
|
||||||
|
import type { TransactionFull } from '../../../shared/types.ts'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
objectId: number
|
objectId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const Result: FunctionalComponent<Props> = ({ objectId }) => {
|
const Result: FunctionalComponent<Props> = ({ objectId }) => {
|
||||||
const [transactions, setTransactions] = useState([])
|
const [transactions, setTransactions] = useState<TransactionFull[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/objects/${objectId}`).then(setTransactions)
|
rek(`/api/objects/${objectId}`).then(setTransactions)
|
||||||
|
|||||||
@ -1,16 +1,17 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
import Head from './head.ts'
|
import Head from './head.ts'
|
||||||
import Object from './object.tsx'
|
import Object from './object.tsx'
|
||||||
import s from './results_page.module.scss'
|
import s from './results_page.module.scss'
|
||||||
|
import type { Object as ObjectType } from '../../../shared/types.ts'
|
||||||
|
|
||||||
const ObjectsPage = () => {
|
const ObjectsPage: FunctionComponent = () => {
|
||||||
const [objects, setObjects] = useState([])
|
const [objects, setObjects] = useState<ObjectType[]>([])
|
||||||
const [currentObject, setCurrentObject] = useState(null)
|
const [currentObject, setCurrentObject] = useState<ObjectType | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek('/api/objects').then(setObjects)
|
rek('/api/objects').then((objects: ObjectType[]) => setObjects(objects))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useEffect, useState } from 'preact/hooks'
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import rek from 'rek'
|
import rek from 'rek'
|
||||||
@ -6,13 +6,15 @@ import Head from './head.ts'
|
|||||||
import { formatNumber } from '../utils/format_number.ts'
|
import { formatNumber } from '../utils/format_number.ts'
|
||||||
import s from './results_page.module.scss'
|
import s from './results_page.module.scss'
|
||||||
|
|
||||||
const ResultsPage = () => {
|
import type { FinancialYear, Result } from '../../../shared/types.ts'
|
||||||
const [results, setResults] = useState([])
|
|
||||||
|
const ResultsPage: FunctionComponent = () => {
|
||||||
|
const [results, setResults] = useState<Result[]>([])
|
||||||
const [years, setYears] = useState<number[]>([])
|
const [years, setYears] = useState<number[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek(`/api/results`).then(setResults)
|
rek(`/api/results`).then((results: Result[]) => setResults(results))
|
||||||
rek(`/api/financial-years`).then((years) => setYears(years.map((fy) => fy.year)))
|
rek(`/api/financial-years`).then((financialYears: FinancialYear[]) => setYears(financialYears.map((fy) => fy.year)))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
|
||||||
const StartPage = () => (
|
const StartPage: FunctionComponent = () => (
|
||||||
<section>
|
<section>
|
||||||
<h1>Fart Page</h1>
|
<h1>Fart Page</h1>
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useCallback, useEffect, useState } from 'preact/hooks'
|
import { useCallback, useEffect, useState } from 'preact/hooks'
|
||||||
import { useLocation } from 'preact-iso'
|
import { useLocation } from 'preact-iso'
|
||||||
import { isEmpty } from 'lowline'
|
import { isEmpty } from 'lowline'
|
||||||
@ -8,16 +8,18 @@ import Head from './head.ts'
|
|||||||
import serializeForm from '../../shared/utils/serialize_form.ts'
|
import serializeForm from '../../shared/utils/serialize_form.ts'
|
||||||
import { formatNumber } from '../utils/format_number.ts'
|
import { formatNumber } from '../utils/format_number.ts'
|
||||||
|
|
||||||
const TransactionsPage = () => {
|
import type { TransactionFull, FinancialYear } from '../../../shared/types.ts'
|
||||||
const [financialYears, setFinancialYears] = useState([])
|
|
||||||
const [transactions, setTransactions] = useState([])
|
const TransactionsPage: FunctionComponent = () => {
|
||||||
|
const [financialYears, setFinancialYears] = useState<FinancialYear[]>([])
|
||||||
|
const [transactions, setTransactions] = useState<TransactionFull[]>([])
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
||||||
const onSubmit = useCallback((e: SubmitEvent) => {
|
const onSubmit = useCallback((e: SubmitEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const values = serializeForm(e.target as HTMLFormElement)
|
const values = serializeForm(e.target as HTMLFormElement) as Record<string, string>
|
||||||
|
|
||||||
const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
|
const search = !isEmpty(values) ? '?' + qs.stringify(values) : ''
|
||||||
|
|
||||||
@ -25,7 +27,7 @@ const TransactionsPage = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
rek('/api/financial-years').then((financialYears) => {
|
rek('/api/financial-years').then((financialYears: FinancialYear[]) => {
|
||||||
setFinancialYears(financialYears)
|
setFinancialYears(financialYears)
|
||||||
})
|
})
|
||||||
}, [])
|
}, [])
|
||||||
@ -33,7 +35,7 @@ const TransactionsPage = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const search = location.url.split('?')[1] || ''
|
const search = location.url.split('?')[1] || ''
|
||||||
|
|
||||||
rek(`/api/transactions${search ? '?' + search : ''}`).then((transactions) => {
|
rek(`/api/transactions${search ? '?' + search : ''}`).then((transactions: TransactionFull[]) => {
|
||||||
setTransactions(transactions)
|
setTransactions(transactions)
|
||||||
})
|
})
|
||||||
}, [location.url])
|
}, [location.url])
|
||||||
|
|||||||
@ -5,7 +5,7 @@ const priceFormatter = new Intl.NumberFormat('sv-SE', {
|
|||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const formatPrice = (price) => priceFormatter.format(price)
|
export const formatPrice = (price: number) => priceFormatter.format(price)
|
||||||
|
|
||||||
const numberFormatter = new Intl.NumberFormat('sv-SE', {
|
const numberFormatter = new Intl.NumberFormat('sv-SE', {
|
||||||
style: 'decimal',
|
style: 'decimal',
|
||||||
@ -13,4 +13,4 @@ const numberFormatter = new Intl.NumberFormat('sv-SE', {
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const formatNumber = (nbr) => numberFormatter.format(nbr)
|
export const formatNumber = (nbr: number) => numberFormatter.format(nbr)
|
||||||
|
|||||||
@ -1,24 +1,47 @@
|
|||||||
import { h, type FunctionComponent, type PointerEventHandler } from 'preact'
|
import { h, type FunctionComponent, type PointerEventHandler } from 'preact'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
export default function buttonFactory({ defaults, icons, styles }) {
|
type Styles<C extends string, D extends string, S extends string> = {
|
||||||
const Button: FunctionComponent<{
|
base: string
|
||||||
autoHeight?: boolean
|
autoHeight?: string
|
||||||
className?: string
|
fullWidth?: string
|
||||||
color?: string
|
icon?: string
|
||||||
design?: string
|
invert?: string
|
||||||
disabled?: boolean
|
} & Record<C, string> &
|
||||||
fullWidth?: boolean
|
Record<D, string> &
|
||||||
icon?: string
|
Record<S, string>
|
||||||
iconSize?: string
|
|
||||||
invert?: boolean
|
type Props<C extends string, D extends string, I extends string, S extends string> = {
|
||||||
onClick?: PointerEventHandler<HTMLButtonElement>
|
autoHeight?: boolean
|
||||||
size?: string
|
className?: string
|
||||||
tabIndex?: number
|
color?: C
|
||||||
tag?: string
|
design?: D
|
||||||
title?: string
|
disabled?: boolean
|
||||||
type?: 'button' | 'reset' | 'submit'
|
fullWidth?: boolean
|
||||||
}> = ({
|
icon?: I
|
||||||
|
iconSize?: string
|
||||||
|
invert?: boolean
|
||||||
|
onClick?: PointerEventHandler<HTMLButtonElement>
|
||||||
|
size?: S
|
||||||
|
tabIndex?: number
|
||||||
|
tag?: string
|
||||||
|
title?: string
|
||||||
|
type?: 'button' | 'reset' | 'submit'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options<C extends string, D extends string, I extends string, S extends string> = {
|
||||||
|
defaults: Partial<Props<C, D, I, S>>
|
||||||
|
icons: Record<I, string>
|
||||||
|
styles: Styles<C, D, S>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function buttonFactory<
|
||||||
|
C extends string = never,
|
||||||
|
D extends string = never,
|
||||||
|
I extends string = never,
|
||||||
|
S extends string = never,
|
||||||
|
>({ defaults, icons, styles }: Options<C, D, I, S>) {
|
||||||
|
const Button: FunctionComponent<Props<C, D, I, S>> = ({
|
||||||
autoHeight = defaults?.autoHeight,
|
autoHeight = defaults?.autoHeight,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -1,26 +1,30 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
|
import { type ChangeEvent } from 'preact/compat'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
/**
|
type Styles<S extends string> = {
|
||||||
* @param {{
|
base?: string
|
||||||
* base?: string
|
element?: string
|
||||||
* touched?: string
|
label?: string
|
||||||
* element?: string
|
touched?: string
|
||||||
* label?: string
|
} & Record<S, string>
|
||||||
* }} styles
|
|
||||||
* @returns {import('preact').FunctionComponent<{
|
type Props<S extends string> = {
|
||||||
* autoFocus?: boolean
|
autoFocus?: boolean
|
||||||
* className?: string
|
className?: string
|
||||||
* defaultChecked?: boolean
|
defaultChecked?: any
|
||||||
* disabled?: boolean
|
disabled?: boolean
|
||||||
* name: string
|
label?: string
|
||||||
* label?: string
|
name?: string
|
||||||
* required?: boolean
|
onChange?: (e: ChangeEvent) => void
|
||||||
* }>}
|
required?: boolean
|
||||||
*/
|
style: S
|
||||||
export default function checkboxFactory(styles) {
|
value?: string
|
||||||
const Checkbox = ({
|
}
|
||||||
|
|
||||||
|
export default function checkboxFactory<S extends string = never>(styles: Styles<S>) {
|
||||||
|
const Checkbox: FunctionComponent<Props<S>> = ({
|
||||||
autoFocus,
|
autoFocus,
|
||||||
className,
|
className,
|
||||||
defaultChecked,
|
defaultChecked,
|
||||||
@ -29,16 +33,16 @@ export default function checkboxFactory(styles) {
|
|||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
required,
|
required,
|
||||||
style = 'normal',
|
style,
|
||||||
value = 'true',
|
value = 'true',
|
||||||
}) => {
|
}) => {
|
||||||
const [touched, setTouched] = useState(false)
|
const [touched, setTouched] = useState(false)
|
||||||
const checkboxRef = useRef<HTMLInputElement>()
|
const checkboxRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const onBlur = useCallback(() => setTouched(true), [])
|
const onBlur = useCallback(() => setTouched(true), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const form = checkboxRef.current.form
|
const form = checkboxRef.current!.form!
|
||||||
const resetTouched = setTouched.bind(null, false)
|
const resetTouched = setTouched.bind(null, false)
|
||||||
|
|
||||||
form.addEventListener('reset', resetTouched)
|
form.addEventListener('reset', resetTouched)
|
||||||
@ -47,7 +51,7 @@ export default function checkboxFactory(styles) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus) checkboxRef.current.focus()
|
if (autoFocus) checkboxRef.current!.focus()
|
||||||
}, [autoFocus])
|
}, [autoFocus])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,11 +1,36 @@
|
|||||||
import { h } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import { useCallback, useRef, useState } from 'preact/hooks'
|
import { useCallback, useRef, useState } from 'preact/hooks'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
// TODO when form resets after update the previous image flashes quickly after setImageSrc(null) and before new defaultValue propagates
|
// TODO when form resets after update the previous image flashes quickly after setImageSrc(null) and before new defaultValue propagates
|
||||||
|
|
||||||
const fileUploadFactory = ({ styles }) => {
|
type Styles = {
|
||||||
const FileUpload = ({
|
base?: string
|
||||||
|
element?: string
|
||||||
|
label?: string
|
||||||
|
noMargin?: string
|
||||||
|
touched?: string
|
||||||
|
image?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
styles: Styles
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
autoFocus?: boolean
|
||||||
|
className?: string
|
||||||
|
defaultValue?: any
|
||||||
|
disabled?: boolean
|
||||||
|
label?: string
|
||||||
|
name?: string
|
||||||
|
noMargin?: boolean
|
||||||
|
placeholder?: string
|
||||||
|
required?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function fileUploadFactory({ styles }: Options) {
|
||||||
|
const FileUpload: FunctionComponent<Props> = ({
|
||||||
autoFocus,
|
autoFocus,
|
||||||
className,
|
className,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
@ -17,14 +42,14 @@ const fileUploadFactory = ({ styles }) => {
|
|||||||
required,
|
required,
|
||||||
}) => {
|
}) => {
|
||||||
const [touched, setTouched] = useState(false)
|
const [touched, setTouched] = useState(false)
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const imgRef = useRef(null)
|
const imgRef = useRef<HTMLImageElement>(null)
|
||||||
const [imageSrc, setImageSrc] = useState(null)
|
const [imageSrc, setImageSrc] = useState<string | null>(null)
|
||||||
|
|
||||||
const onImageChange = useCallback((e) => {
|
const onImageChange = useCallback((e: Event) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
const form = inputRef.current.form
|
const form = inputRef.current!.form!
|
||||||
|
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
setTouched(false)
|
setTouched(false)
|
||||||
@ -33,8 +58,8 @@ const fileUploadFactory = ({ styles }) => {
|
|||||||
|
|
||||||
form.addEventListener('reset', onReset)
|
form.addEventListener('reset', onReset)
|
||||||
|
|
||||||
imgRef.current.addEventListener('load', () => URL.revokeObjectURL(imgRef.current.src), { once: true })
|
imgRef.current!.addEventListener('load', () => URL.revokeObjectURL(imgRef.current!.src), { once: true })
|
||||||
setImageSrc(URL.createObjectURL(e.currentTarget.files[0]))
|
setImageSrc(URL.createObjectURL((e.currentTarget! as HTMLInputElement).files![0]))
|
||||||
|
|
||||||
return () => form.removeEventListerner('reset', onReset)
|
return () => form.removeEventListerner('reset', onReset)
|
||||||
}, [])
|
}, [])
|
||||||
@ -66,5 +91,3 @@ const fileUploadFactory = ({ styles }) => {
|
|||||||
|
|
||||||
return FileUpload
|
return FileUpload
|
||||||
}
|
}
|
||||||
|
|
||||||
export default fileUploadFactory
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import escapeRegex from '../utils/escape_regex.ts'
|
import escapeRegex from '../utils/escape_regex.ts'
|
||||||
|
|
||||||
export default function inputFactory(styles: {
|
type Styles<C extends string, D extends string, S extends string> = {
|
||||||
base?: string
|
base?: string
|
||||||
touched?: string
|
touched?: string
|
||||||
element?: string
|
element?: string
|
||||||
@ -13,22 +13,38 @@ export default function inputFactory(styles: {
|
|||||||
icon?: string
|
icon?: string
|
||||||
labelIcon?: string
|
labelIcon?: string
|
||||||
center?: string
|
center?: string
|
||||||
}): FunctionComponent<{
|
} & Record<C, string> &
|
||||||
autoFocus?: boolean
|
Record<D, string> &
|
||||||
|
Record<S, string>
|
||||||
|
|
||||||
|
type Props<C extends string, D extends string, S extends string> = {
|
||||||
autoComplete?: string
|
autoComplete?: string
|
||||||
|
autoFocus?: boolean
|
||||||
|
center?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
classNames?: Partial<Styles<C, D, S>>
|
||||||
|
color: C
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
|
design: D
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
icon: string
|
||||||
|
label: string
|
||||||
name: string
|
name: string
|
||||||
noMargin?: boolean
|
noMargin?: boolean
|
||||||
label: string
|
|
||||||
pattern?: string
|
pattern?: string
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
type?: string
|
sameAs?: string
|
||||||
showValidity?: boolean
|
showValidity?: boolean
|
||||||
}> {
|
size?: S
|
||||||
const Input = ({
|
type?: string
|
||||||
|
value?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function inputFactory<C extends string = never, D extends string = never, S extends string = never>(
|
||||||
|
styles: Styles<C, D, S>,
|
||||||
|
) {
|
||||||
|
const Input: FunctionComponent<Props<C, D, S>> = ({
|
||||||
autoComplete,
|
autoComplete,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
center,
|
center,
|
||||||
@ -46,28 +62,27 @@ export default function inputFactory(styles: {
|
|||||||
placeholder,
|
placeholder,
|
||||||
required,
|
required,
|
||||||
sameAs,
|
sameAs,
|
||||||
size = 'medium',
|
size,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
value,
|
value,
|
||||||
showValidity = true,
|
showValidity = true,
|
||||||
}) => {
|
}) => {
|
||||||
const [touched, setTouched] = useState(false)
|
const [touched, setTouched] = useState(false)
|
||||||
const inputRef = useRef<HTMLInputElement>()
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const onBlur = useCallback(() => setTouched(true), [])
|
const onBlur = useCallback(() => setTouched(true), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onInput(e) {
|
function onInput(e: Event) {
|
||||||
inputRef.current!.pattern = escapeRegex(e.target.value)
|
inputRef.current!.pattern = escapeRegex((e.target! as HTMLInputElement).value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = inputRef.current!.form
|
const form = inputRef.current!.form!
|
||||||
const resetTouched = setTouched.bind(null, false)
|
const resetTouched = setTouched.bind(null, false)
|
||||||
|
|
||||||
form.addEventListener('reset', resetTouched)
|
form.addEventListener('reset', resetTouched)
|
||||||
|
|
||||||
let sameAsInput
|
let sameAsInput: HTMLInputElement
|
||||||
|
|
||||||
if (sameAs) {
|
if (sameAs) {
|
||||||
sameAsInput = form[sameAs]
|
sameAsInput = form[sameAs]
|
||||||
sameAsInput.addEventListener('input', onInput)
|
sameAsInput.addEventListener('input', onInput)
|
||||||
@ -80,7 +95,7 @@ export default function inputFactory(styles: {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoFocus) inputRef.current.focus()
|
if (autoFocus) inputRef.current!.focus()
|
||||||
}, [autoFocus])
|
}, [autoFocus])
|
||||||
|
|
||||||
const id = styles.element + '_' + name
|
const id = styles.element + '_' + name
|
||||||
|
|||||||
@ -1,21 +1,48 @@
|
|||||||
import { h, type FunctionComponent } from 'preact'
|
import { h, type FunctionComponent } from 'preact'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
|
|
||||||
export default function linkButtonFactory({ defaults, icons, styles }) {
|
type Styles<C extends string, D extends string, S extends string> = {
|
||||||
const LinkButton: FunctionComponent<{
|
base?: string
|
||||||
autoHeight?: boolean
|
touched?: string
|
||||||
className?: string
|
element?: string
|
||||||
color?: string
|
label?: string
|
||||||
design?: string
|
icon?: string
|
||||||
href?: string
|
|
||||||
fullWidth?: boolean
|
autoHeight?: string
|
||||||
icon?: string
|
fullWidth?: string
|
||||||
iconSize?: string
|
invert?: string
|
||||||
invert?: boolean
|
} & Record<C, string> &
|
||||||
size?: string
|
Record<D, string> &
|
||||||
tabIndex?: number
|
Record<S, string>
|
||||||
title?: string
|
|
||||||
}> = ({
|
type Options<C extends string, D extends string, I extends string, S extends string> = {
|
||||||
|
defaults: Pick<Props<C, D, I, S>, 'autoHeight' | 'color' | 'design' | 'fullWidth' | 'icon' | 'invert' | 'size'>
|
||||||
|
icons: Record<I, string>
|
||||||
|
styles: Styles<C, D, S>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props<C extends string, D extends string, I extends string, S extends string> = {
|
||||||
|
autoHeight?: boolean
|
||||||
|
className?: string
|
||||||
|
color?: C
|
||||||
|
design?: D
|
||||||
|
href?: string
|
||||||
|
fullWidth?: boolean
|
||||||
|
icon?: I
|
||||||
|
iconSize?: string
|
||||||
|
invert?: boolean
|
||||||
|
size?: S
|
||||||
|
tabIndex?: number
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function linkButtonFactory<
|
||||||
|
C extends string = never,
|
||||||
|
D extends string = never,
|
||||||
|
I extends string = never,
|
||||||
|
S extends string = never,
|
||||||
|
>({ defaults, icons, styles }: Options<C, D, I, S>) {
|
||||||
|
const LinkButton: FunctionComponent<Props<C, D, I, S>> = ({
|
||||||
autoHeight = defaults?.autoHeight,
|
autoHeight = defaults?.autoHeight,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
|
import { type ContainerNode, type FunctionComponent } from 'preact'
|
||||||
import { createPortal } from 'preact/compat'
|
import { createPortal } from 'preact/compat'
|
||||||
|
|
||||||
const Portal = ({ children, container = typeof document !== 'undefined' && document.body }) =>
|
const Portal: FunctionComponent<{ container: ContainerNode }> = ({
|
||||||
createPortal(children, container)
|
children,
|
||||||
|
container = typeof document !== 'undefined' && document.body,
|
||||||
|
}) => container && createPortal(children, container)
|
||||||
|
|
||||||
export default Portal
|
export default Portal
|
||||||
|
|||||||
@ -2,36 +2,54 @@ import { h, type FunctionComponent } from 'preact'
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import mergeStyles from '../utils/merge_styles.ts'
|
import mergeStyles from '../utils/merge_styles.ts'
|
||||||
|
import { Script } from '@fastify/type-provider-typebox'
|
||||||
|
|
||||||
// interface Styles {
|
type Styles<C extends string, D extends string, S extends string> = {
|
||||||
// base?: string
|
base?: string
|
||||||
// touched?: string
|
touched?: string
|
||||||
// element?: string
|
element?: string
|
||||||
// label?: string
|
label?: string
|
||||||
// }
|
icon?: string
|
||||||
|
} & Record<C, string> &
|
||||||
|
Record<D, string> &
|
||||||
|
Record<S, string>
|
||||||
|
|
||||||
export default function selectFactory({ styles }): FunctionComponent<{
|
type Options<C extends string, D extends string, S extends string> = {
|
||||||
|
styles: Styles<C, D, S>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props<C extends string, D extends string, S extends string> = {
|
||||||
autoFocus?: boolean
|
autoFocus?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
|
color?: C
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
|
design?: D
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
icon: ANY
|
||||||
|
label: string
|
||||||
name: string
|
name: string
|
||||||
noEmpty?: boolean
|
noEmpty?: boolean
|
||||||
label: string
|
onChange?: ANY
|
||||||
|
options: string[] | number[] | { label: string; value: string | number }[]
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
required?: boolean
|
required?: boolean
|
||||||
options: string[] | number[] | { label: string; value: string | number }
|
size: S
|
||||||
}> {
|
}
|
||||||
|
|
||||||
|
// designs / color / size
|
||||||
|
export default function selectFactory<C extends string = never, D extends string = never, S extends string = never>({
|
||||||
|
styles,
|
||||||
|
}: Options<C, D, S>) {
|
||||||
if (Array.isArray(styles)) {
|
if (Array.isArray(styles)) {
|
||||||
styles = mergeStyles(styles)
|
styles = mergeStyles(styles)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Select = ({
|
const Select: FunctionComponent<Props<C, D, S>> = ({
|
||||||
autoFocus,
|
autoFocus,
|
||||||
className,
|
className,
|
||||||
color,
|
color,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
design = 'main',
|
design,
|
||||||
disabled,
|
disabled,
|
||||||
icon,
|
icon,
|
||||||
name,
|
name,
|
||||||
@ -40,11 +58,11 @@ export default function selectFactory({ styles }): FunctionComponent<{
|
|||||||
onChange,
|
onChange,
|
||||||
placeholder,
|
placeholder,
|
||||||
required,
|
required,
|
||||||
size = 'medium',
|
size,
|
||||||
options,
|
options,
|
||||||
}) => {
|
}) => {
|
||||||
const [touched, setTouched] = useState(false)
|
const [touched, setTouched] = useState(false)
|
||||||
const selectRef = useRef<HTMLSelectElement>()
|
const selectRef = useRef<HTMLSelectElement>(null)
|
||||||
|
|
||||||
const onBlur = useCallback(() => setTouched(true), [])
|
const onBlur = useCallback(() => setTouched(true), [])
|
||||||
|
|
||||||
@ -90,10 +108,10 @@ export default function selectFactory({ styles }): FunctionComponent<{
|
|||||||
{options &&
|
{options &&
|
||||||
options.map((option) => (
|
options.map((option) => (
|
||||||
<option
|
<option
|
||||||
key={option.value != null ? option.value : option}
|
key={typeof option === 'object' ? option.value : option}
|
||||||
value={option.value != null ? option.value : option}
|
value={typeof option === 'object' ? option.value : option}
|
||||||
>
|
>
|
||||||
{option.label || option}
|
{typeof option === 'object' ? option.label : option}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@ -3,48 +3,50 @@ import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import mergeStyles from '../utils/merge_styles.ts'
|
import mergeStyles from '../utils/merge_styles.ts'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
autoFocus?: boolean
|
||||||
|
className?: string
|
||||||
|
cols?: number
|
||||||
|
defaultValue?: string
|
||||||
|
disabled?: boolean
|
||||||
|
label: string
|
||||||
|
name: string
|
||||||
|
placeholder?: string
|
||||||
|
required?: boolean
|
||||||
|
rows: number
|
||||||
|
showValidity?: boolean
|
||||||
|
}
|
||||||
export default function textareaFactory(styles: {
|
export default function textareaFactory(styles: {
|
||||||
base?: string
|
base?: string
|
||||||
touched?: string
|
touched?: string
|
||||||
element?: string
|
element?: string
|
||||||
label?: string
|
label?: string
|
||||||
statusIcon?: string
|
statusIcon?: string
|
||||||
}): FunctionComponent<{
|
}): FunctionComponent<Props> {
|
||||||
autoFocus?: boolean
|
|
||||||
className?: string
|
|
||||||
cols?: number
|
|
||||||
defaultValue?: string
|
|
||||||
disabled?: boolean
|
|
||||||
name: string
|
|
||||||
label: string
|
|
||||||
placeholder?: string
|
|
||||||
required?: boolean
|
|
||||||
showValidity?: boolean
|
|
||||||
}> {
|
|
||||||
if (Array.isArray(styles)) {
|
if (Array.isArray(styles)) {
|
||||||
styles = mergeStyles(styles)
|
styles = mergeStyles(styles)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Textarea = ({
|
const Textarea = ({
|
||||||
cols,
|
|
||||||
autoFocus,
|
autoFocus,
|
||||||
className,
|
className,
|
||||||
|
cols,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
disabled,
|
disabled,
|
||||||
name,
|
|
||||||
label,
|
label,
|
||||||
|
name,
|
||||||
placeholder,
|
placeholder,
|
||||||
required,
|
required,
|
||||||
rows,
|
rows,
|
||||||
showValidity = true,
|
showValidity = true,
|
||||||
}) => {
|
}: Props) => {
|
||||||
const [touched, setTouched] = useState(false)
|
const [touched, setTouched] = useState(false)
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>()
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const onBlur = useCallback(() => setTouched(true), [])
|
const onBlur = useCallback(() => setTouched(true), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const form = textareaRef.current!.form
|
const form = textareaRef.current!.form!
|
||||||
const resetTouched = setTouched.bind(null, false)
|
const resetTouched = setTouched.bind(null, false)
|
||||||
|
|
||||||
form.addEventListener('reset', resetTouched)
|
form.addEventListener('reset', resetTouched)
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { useState } from 'preact/hooks'
|
import { useState } from 'preact/hooks'
|
||||||
import isLoading from '../utils/is_loading.ts'
|
import isLoading from '../utils/is_loading.ts'
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
error: Error | null
|
||||||
|
pending: boolean
|
||||||
|
success: boolean
|
||||||
|
response: any
|
||||||
|
}
|
||||||
|
|
||||||
const useRequestState = () => {
|
const useRequestState = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
error: null,
|
error: null,
|
||||||
@ -9,7 +16,7 @@ const useRequestState = () => {
|
|||||||
response: null,
|
response: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const [state, setState] = useState(initialState)
|
const [state, setState] = useState<State>(initialState)
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
reset() {
|
reset() {
|
||||||
@ -18,7 +25,7 @@ const useRequestState = () => {
|
|||||||
setState(initialState)
|
setState(initialState)
|
||||||
},
|
},
|
||||||
|
|
||||||
error(error) {
|
error(error: Error) {
|
||||||
isLoading(false)
|
isLoading(false)
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
const scrollingElement = typeof window !== 'undefined' && (document.scrollingElement || document.documentElement)
|
const scrollingElement = typeof window !== 'undefined' && (document.scrollingElement || document.documentElement)
|
||||||
|
|
||||||
let scrollTop
|
let scrollTop: number
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
disable() {
|
disable() {
|
||||||
@ -15,6 +15,6 @@ export default {
|
|||||||
document.body.style.position = ''
|
document.body.style.position = ''
|
||||||
document.body.style.top = ''
|
document.body.style.top = ''
|
||||||
document.body.style.width = ''
|
document.body.style.width = ''
|
||||||
scrollingElement.scrollTop = scrollTop
|
;(scrollingElement as Element).scrollTop = scrollTop
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export default function escapeRegex(string) {
|
export default function escapeRegex(string: string) {
|
||||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export default (state) => {
|
export default (state: boolean) => {
|
||||||
document.body.classList.toggle('loading', state)
|
document.body.classList.toggle('loading', state)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ export default function mergeStyles(styles: Record<string, string>[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const obj = {}
|
const obj: Record<string, string> = {}
|
||||||
|
|
||||||
for (const prop of props) {
|
for (const prop of props) {
|
||||||
obj[prop] = cn(styles.map((style) => style[prop]))
|
obj[prop] = cn(styles.map((style) => style[prop]))
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
export default function (form: HTMLFormElement, hook?: (...args: ANY[]) => ANY) {
|
export default function (form: HTMLFormElement, hook?: (...args: ANY[]) => ANY) {
|
||||||
const result = {}
|
const result: Record<string, string | string[]> = {}
|
||||||
|
|
||||||
for (const element of form.elements as unknown as HTMLInputElement[]) {
|
for (const element of form.elements as unknown as HTMLInputElement[]) {
|
||||||
const { checked, name, type, value } = element
|
const { checked, name, type, value } = element
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
// copied and modified from https://github.com/sindresorhus/throttleit/blob/ed1d22c70a964ef0299d0400dbfd1fbedef56a59/index.js
|
// copied and modified from https://github.com/sindresorhus/throttleit/blob/ed1d22c70a964ef0299d0400dbfd1fbedef56a59/index.js
|
||||||
|
|
||||||
export default function throttle(fnc, wait) {
|
export default function throttle<F extends (...args: any[]) => any>(fnc: F, wait: number) {
|
||||||
let timeout
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
let lastCallTime = 0
|
let lastCallTime = 0
|
||||||
|
|
||||||
return function throttled(...args) {
|
return function throttled(...args: Parameters<F>) {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
// modified version of: https://github.com/modosc/global-jsdom/blob/d1dd3cdeeeddd4d0653496a728e0f81e18776654/packages/global-jsdom/esm/index.mjs
|
// modified version of: https://github.com/modosc/global-jsdom/blob/d1dd3cdeeeddd4d0653496a728e0f81e18776654/packages/global-jsdom/esm/index.mjs
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
import JSDOM from 'jsdom'
|
import JSDOM from 'jsdom'
|
||||||
|
|
||||||
const html = '<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>'
|
const html = '<!doctype html><html><head><meta charset="utf-8"></head><body></body></html>'
|
||||||
@ -17,6 +18,7 @@ const { document } = window
|
|||||||
|
|
||||||
const KEYS = Object.getOwnPropertyNames(window).filter((k) => !k.startsWith('_') && !(k in globalThis))
|
const KEYS = Object.getOwnPropertyNames(window).filter((k) => !k.startsWith('_') && !(k in globalThis))
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
KEYS.forEach((key) => (globalThis[key] = window[key]))
|
KEYS.forEach((key) => (globalThis[key] = window[key]))
|
||||||
|
|
||||||
globalThis.document = document
|
globalThis.document = document
|
||||||
|
|||||||
@ -48,6 +48,7 @@
|
|||||||
"@babel/core": "^7.26.10",
|
"@babel/core": "^7.26.10",
|
||||||
"@preact/preset-vite": "^2.10.1",
|
"@preact/preset-vite": "^2.10.1",
|
||||||
"@testing-library/preact": "^3.2.4",
|
"@testing-library/preact": "^3.2.4",
|
||||||
|
"@types/d3-dsv": "^3.0.7",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
"@typescript/native-preview": "7.0.0-dev.20251126.1",
|
"@typescript/native-preview": "7.0.0-dev.20251126.1",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -78,6 +78,9 @@ importers:
|
|||||||
'@testing-library/preact':
|
'@testing-library/preact':
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.4(preact@10.27.2)
|
version: 3.2.4(preact@10.27.2)
|
||||||
|
'@types/d3-dsv':
|
||||||
|
specifier: ^3.0.7
|
||||||
|
version: 3.0.7
|
||||||
'@types/lodash':
|
'@types/lodash':
|
||||||
specifier: ^4.17.16
|
specifier: ^4.17.16
|
||||||
version: 4.17.20
|
version: 4.17.20
|
||||||
@ -924,6 +927,9 @@ packages:
|
|||||||
'@types/aria-query@5.0.4':
|
'@types/aria-query@5.0.4':
|
||||||
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==}
|
||||||
|
|
||||||
|
'@types/d3-dsv@3.0.7':
|
||||||
|
resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@ -2949,6 +2955,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/aria-query@5.0.4': {}
|
'@types/aria-query@5.0.4': {}
|
||||||
|
|
||||||
|
'@types/d3-dsv@3.0.7': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/lodash@4.17.20': {}
|
'@types/lodash@4.17.20': {}
|
||||||
|
|||||||
@ -1,32 +1,26 @@
|
|||||||
const evals = ['false', 'true', 'null', 'undefined']
|
function read<
|
||||||
const numberRegex = /^\d+$/
|
const C extends readonly string[],
|
||||||
|
D extends Partial<Record<C[number], any>> & Record<Exclude<keyof D, C[number]>, never> = {},
|
||||||
|
>(columns: C, defaults: D): Record<C[number], any> {
|
||||||
|
const missing: string[] = []
|
||||||
|
|
||||||
export function read(columns, defaults = {}) {
|
const result: Record<string, any> = {}
|
||||||
columns = [...new Set(columns.concat(Object.keys(defaults)))].sort()
|
|
||||||
|
|
||||||
const missing = []
|
for (const column of columns) {
|
||||||
|
let value: string | number | boolean | null | undefined = process.env[column]
|
||||||
const result = columns.reduce((result, variable) => {
|
|
||||||
/* eslint-disable-next-line n/no-process-env */
|
|
||||||
let value = process.env[variable] || defaults[variable]
|
|
||||||
|
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
missing.push(variable)
|
value = defaults[column as keyof D] ?? null
|
||||||
|
missing.push(column)
|
||||||
} else {
|
} else {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
if (numberRegex.test(value)) {
|
if (/^\d+$/.test(value)) value = Number(value)
|
||||||
value = Number.parseInt(value)
|
else if (['false', 'true', 'null', 'undefined'].includes(value)) value = eval(value)
|
||||||
} else if (evals.includes(value)) {
|
|
||||||
// eslint-disable-next-line no-eval
|
|
||||||
value = eval(value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result[variable] = value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
result[column] = value
|
||||||
}, {})
|
}
|
||||||
|
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
throw new Error(`Missing required env variables: ${missing.join(', ')}`)
|
throw new Error(`Missing required env variables: ${missing.join(', ')}`)
|
||||||
@ -52,7 +46,7 @@ export default read(
|
|||||||
'PGPORT',
|
'PGPORT',
|
||||||
'PGUSER',
|
'PGUSER',
|
||||||
'REDIS_HOST',
|
'REDIS_HOST',
|
||||||
],
|
] as const,
|
||||||
{
|
{
|
||||||
PGPASSWORD: null,
|
PGPASSWORD: null,
|
||||||
PGPORT: null,
|
PGPORT: null,
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { DatabaseError } from 'pg-protocol'
|
import { DatabaseError } from 'pg-protocol'
|
||||||
import StatusError from '../lib/status_error.ts'
|
import StatusError from '../lib/status_error.ts'
|
||||||
|
import { type ErrorHandler } from '../types.ts'
|
||||||
|
|
||||||
const databaseErrorCodes = {
|
const databaseErrorCodes: Record<string, number> = {
|
||||||
23505: 409,
|
23505: 409,
|
||||||
23514: 409,
|
23514: 409,
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultHandler(error, request, reply) {
|
const defaultHandler: ErrorHandler = (error, _request, reply) => {
|
||||||
return reply
|
return reply
|
||||||
.type('text/html')
|
.type('text/html')
|
||||||
.send(
|
.send(
|
||||||
@ -14,21 +15,24 @@ function defaultHandler(error, request, reply) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ErrorHandler(handler = defaultHandler) {
|
export default function createErrorHandler(handler: ErrorHandler = defaultHandler): ErrorHandler {
|
||||||
return async function errorHandler(error, request, reply) {
|
return async function errorHandler(error, request, reply) {
|
||||||
if (error instanceof DatabaseError && error.code in databaseErrorCodes) {
|
if (error instanceof DatabaseError && error.code && error.code in databaseErrorCodes) {
|
||||||
error = new StatusError(databaseErrorCodes[error.code], error.detail || 'Database Error', { cause: error })
|
error = new StatusError(databaseErrorCodes[error.code], error.detail || 'Database Error', { cause: error })
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.log.error({ req: request, err: error }, error?.message)
|
reply.log.error({ req: request, err: error }, error?.message)
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
reply.status(error.status || error.statusCode || 500)
|
reply.status(error.status || error.statusCode || 500)
|
||||||
|
|
||||||
if (request.headers.accept?.includes('application/json')) {
|
if (request.headers.accept?.includes('application/json')) {
|
||||||
return reply.send(
|
return reply.send(
|
||||||
|
// @ts-ignore
|
||||||
error.toJSON?.() || {
|
error.toJSON?.() || {
|
||||||
name: error.name,
|
name: error.name,
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
// @ts-ignore
|
||||||
status: error.status || error.statusCode || 500,
|
status: error.status || error.statusCode || 500,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { RouteHandler } from 'fastify'
|
||||||
import env from '../env.ts'
|
import env from '../env.ts'
|
||||||
|
|
||||||
const CONTENT = {
|
const CONTENT = {
|
||||||
@ -11,8 +12,10 @@ Disallow: /admin
|
|||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
|
|
||||||
const contents = (CONTENT[env.NODE_ENV] || CONTENT.development).trim()
|
const contents = (CONTENT[env.NODE_ENV as 'development' | 'production'] || CONTENT.development).trim()
|
||||||
|
|
||||||
export default function robots(_request, reply) {
|
const robots: RouteHandler = (_request, reply) => {
|
||||||
return reply.type('text/plain').send(contents)
|
return reply.type('text/plain').send(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default robots
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import env from '../env.ts'
|
|||||||
|
|
||||||
if (env.NODE_ENV === 'development') {
|
if (env.NODE_ENV === 'development') {
|
||||||
// output filename in console log and colour console.dir
|
// output filename in console log and colour console.dir
|
||||||
|
// @ts-ignore
|
||||||
await import('@bmp/console').then((mod) => mod.default({ log: true, error: true, dir: true }))
|
await import('@bmp/console').then((mod) => mod.default({ log: true, error: true, dir: true }))
|
||||||
|
|
||||||
const { default: chalk } = await import('chalk')
|
const { default: chalk } = await import('chalk')
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { Readable } from 'node:stream'
|
|||||||
|
|
||||||
type ExpressionPrimitives = string | number | Date | boolean | undefined | null | ExpressionPrimitives[]
|
type ExpressionPrimitives = string | number | Date | boolean | undefined | null | ExpressionPrimitives[]
|
||||||
|
|
||||||
type Expression = ExpressionPrimitives | (() => ExpressionPrimitives) | Promise<ExpressionPrimitives>
|
type Expression = ExpressionPrimitives | (() => ExpressionPrimitives) | Promise<Expression>
|
||||||
|
|
||||||
type ParseGenerator = Generator<ExpressionPrimitives, void>
|
type ParseGenerator = Generator<ExpressionPrimitives | Promise<ExpressionPrimitives>, void>
|
||||||
|
|
||||||
export interface Template {
|
export interface Template {
|
||||||
generator: (minify?: boolean) => ParseGenerator
|
generator: (minify?: boolean) => ParseGenerator
|
||||||
@ -14,16 +14,17 @@ export interface Template {
|
|||||||
|
|
||||||
const cache = new WeakMap()
|
const cache = new WeakMap()
|
||||||
|
|
||||||
function apply(expression: Expression) {
|
function apply(expression: Expression): ExpressionPrimitives | Promise<ExpressionPrimitives> {
|
||||||
if (!expression) {
|
if (expression == null) {
|
||||||
return ''
|
return ''
|
||||||
} else if ((expression as Promise<ExpressionPrimitives>).then) {
|
} else if (expression instanceof Promise) {
|
||||||
return (expression as Promise<ExpressionPrimitives>).then(apply)
|
return expression.then((resolved) => apply(resolved))
|
||||||
} else if (typeof expression === 'function') {
|
} else if (typeof expression === 'function') {
|
||||||
return apply(expression())
|
return apply(expression())
|
||||||
} else {
|
} else if (Array.isArray(expression)) {
|
||||||
return (expression as ExpressionPrimitives[]).join?.('') ?? expression
|
return expression.join('')
|
||||||
}
|
}
|
||||||
|
return expression
|
||||||
}
|
}
|
||||||
|
|
||||||
function* parse(strings: TemplateStringsArray, expressions: Expression[], minify?: boolean): ParseGenerator {
|
function* parse(strings: TemplateStringsArray, expressions: Expression[], minify?: boolean): ParseGenerator {
|
||||||
@ -67,8 +68,8 @@ function stream(generator: ParseGenerator) {
|
|||||||
async function text(generator: ParseGenerator) {
|
async function text(generator: ParseGenerator) {
|
||||||
let result = ''
|
let result = ''
|
||||||
|
|
||||||
for await (const chunk of generator) {
|
for (const chunk of generator) {
|
||||||
result += chunk
|
result += chunk instanceof Promise ? await chunk : chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
// eslint-disable-next-line import/no-named-as-default
|
|
||||||
import knex from 'knex'
|
import knex from 'knex'
|
||||||
|
// @ts-ignore
|
||||||
import pg from 'pg'
|
import pg from 'pg'
|
||||||
import env from '../env.ts'
|
import env from '../env.ts'
|
||||||
|
|
||||||
@ -8,8 +8,8 @@ const queryProto = pg.Query.prototype
|
|||||||
|
|
||||||
const handleRowDescription = queryProto.handleRowDescription
|
const handleRowDescription = queryProto.handleRowDescription
|
||||||
|
|
||||||
queryProto.handleRowDescription = function (msg) {
|
queryProto.handleRowDescription = function (msg: { fields: { name: string }[] }) {
|
||||||
msg.fields.forEach((field) => {
|
msg.fields.forEach((field: { name: string }) => {
|
||||||
field.name = _.camelCase(field.name)
|
field.name = _.camelCase(field.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -110,9 +110,18 @@ test('parseSRU', (t: TestContext) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('parseTrans', (t: TestContext) => {
|
test('parseTrans', (t: TestContext) => {
|
||||||
|
type Expected = {
|
||||||
|
accountNumber: number
|
||||||
|
objectList: [number, number][] | null
|
||||||
|
amount: number
|
||||||
|
transactionDate: string | null
|
||||||
|
description: string | null
|
||||||
|
quantity: number | null
|
||||||
|
signature: null
|
||||||
|
}
|
||||||
// Fortnox 3.57.11
|
// Fortnox 3.57.11
|
||||||
let line = '#TRANS 1790 {1 "1"} 7509 "" "Faktura 9631584500436 172-57 - Perspektiv Bredband AB" 0'
|
let line = '#TRANS 1790 {1 "1"} 7509 "" "Faktura 9631584500436 172-57 - Perspektiv Bredband AB" 0'
|
||||||
let expected = {
|
let expected: Expected = {
|
||||||
accountNumber: 1790,
|
accountNumber: 1790,
|
||||||
objectList: [[1, 1]],
|
objectList: [[1, 1]],
|
||||||
amount: 7509,
|
amount: 7509,
|
||||||
|
|||||||
@ -3,7 +3,14 @@
|
|||||||
const rDim = /#DIM\s+(-?\d+)\s+"([^"]*)"$/
|
const rDim = /#DIM\s+(-?\d+)\s+"([^"]*)"$/
|
||||||
|
|
||||||
export function parseDim(line: string) {
|
export function parseDim(line: string) {
|
||||||
const [, number, name] = line.match(rDim)
|
const result = line.match(rDim)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.error(line)
|
||||||
|
throw Error('parsing error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, number, name] = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: parseInt(number),
|
number: parseInt(number),
|
||||||
@ -30,7 +37,7 @@ export function parseIB(line: string) {
|
|||||||
accountNumber: parseInt(accountNumber),
|
accountNumber: parseInt(accountNumber),
|
||||||
yearNumber: parseInt(yearNumber),
|
yearNumber: parseInt(yearNumber),
|
||||||
balance: parseFloat(balance),
|
balance: parseFloat(balance),
|
||||||
quantity: parseInt(quantity),
|
quantity: quantity ? parseInt(quantity) : null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +52,7 @@ export function parseKonto(line: string) {
|
|||||||
throw Error('parsing error')
|
throw Error('parsing error')
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, number, description] = line.match(rKonto)
|
const [, number, description] = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: parseInt(number),
|
number: parseInt(number),
|
||||||
@ -58,7 +65,14 @@ export function parseKonto(line: string) {
|
|||||||
const rObjekt = /^#OBJEKT\s+(\d+)\s+"?(\d+)"?\s"([^"]*)"$/
|
const rObjekt = /^#OBJEKT\s+(\d+)\s+"?(\d+)"?\s"([^"]*)"$/
|
||||||
|
|
||||||
export function parseObjekt(line: string) {
|
export function parseObjekt(line: string) {
|
||||||
const [, dimensionNumber, number, name] = line.match(rObjekt)
|
const result = line.match(rObjekt)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.error(line)
|
||||||
|
throw Error('parsing error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, dimensionNumber, number, name] = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dimensionNumber: parseInt(dimensionNumber),
|
dimensionNumber: parseInt(dimensionNumber),
|
||||||
@ -73,7 +87,14 @@ export function parseObjekt(line: string) {
|
|||||||
const rRAR = /^#RAR\s+(-?\d+)\s+(\d{8,8})\s+(\d{8,8})$/
|
const rRAR = /^#RAR\s+(-?\d+)\s+(\d{8,8})\s+(\d{8,8})$/
|
||||||
|
|
||||||
export function parseRAR(line: string) {
|
export function parseRAR(line: string) {
|
||||||
const [, yearNumber, startDate, endDate] = line.match(rRAR)
|
const result = line.match(rRAR)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.error(line)
|
||||||
|
throw Error('parsing error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, yearNumber, startDate, endDate] = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
yearNumber: parseInt(yearNumber),
|
yearNumber: parseInt(yearNumber),
|
||||||
@ -87,7 +108,14 @@ export function parseRAR(line: string) {
|
|||||||
const rSRU = /^#SRU\s+(\d{4,4})\s+(\d{4,4})$/
|
const rSRU = /^#SRU\s+(\d{4,4})\s+(\d{4,4})$/
|
||||||
|
|
||||||
export function parseSRU(line: string) {
|
export function parseSRU(line: string) {
|
||||||
const [, number, sru] = line.match(rSRU)
|
const result = line.match(rSRU)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.error(line)
|
||||||
|
throw Error('parsing error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, number, sru] = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
number: parseInt(number),
|
number: parseInt(number),
|
||||||
@ -112,16 +140,16 @@ export function parseTrans(line: string) {
|
|||||||
|
|
||||||
const [, accountNumber, objectListString, amount, transactionDate, description, quantity, signature] = result
|
const [, accountNumber, objectListString, amount, transactionDate, description, quantity, signature] = result
|
||||||
|
|
||||||
let objectList = null
|
let objectList: [number, number][] | null = null
|
||||||
|
|
||||||
if (objectListString) {
|
if (objectListString) {
|
||||||
objectList = []
|
objectList = []
|
||||||
const result = objectListString.matchAll(rObjectList)
|
const result = objectListString.matchAll(rObjectList)
|
||||||
|
|
||||||
for (const match of result) {
|
for (const match of result) {
|
||||||
const [, dimension, object] = match
|
const [, dimension, object] = match as string[]
|
||||||
|
|
||||||
objectList.push([dimension, object].map((val) => parseInt(val)))
|
objectList.push([dimension, object].map((val) => parseInt(val)) as [number, number])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +192,14 @@ export function parseUB(line: string) {
|
|||||||
const rVer = /#VER\s+(\w+)\s+(\d+)\s+(\d{8,8})\s+"([^"]*)"\s+(\d{8,8})(?:\s+(.*))?/
|
const rVer = /#VER\s+(\w+)\s+(\d+)\s+(\d{8,8})\s+"([^"]*)"\s+(\d{8,8})(?:\s+(.*))?/
|
||||||
|
|
||||||
export function parseVer(line: string) {
|
export function parseVer(line: string) {
|
||||||
const [, journal, number, transactionDate, description, entryDate, signature] = line.match(rVer)
|
const result = line.match(rVer)
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
console.error(line)
|
||||||
|
throw Error('parsing error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, journal, number, transactionDate, description, entryDate, signature] = result
|
||||||
|
|
||||||
return {
|
return {
|
||||||
journal,
|
journal,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type ReadableStream } from 'node:stream/web'
|
import type { ReadableStream } from 'stream/web'
|
||||||
import knex from './knex.ts'
|
import knex from './knex.ts'
|
||||||
|
|
||||||
import split, { type Decoder } from './split.ts'
|
import split, { type Decoder } from './split.ts'
|
||||||
@ -41,7 +41,7 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
|
|||||||
const journals = new Map()
|
const journals = new Map()
|
||||||
|
|
||||||
let currentEntry: { id: number; description: string }
|
let currentEntry: { id: number; description: string }
|
||||||
let currentInvoiceId: number
|
let currentInvoiceId: number | null
|
||||||
let currentYear = null
|
let currentYear = null
|
||||||
const details: Record<string, string> = {}
|
const details: Record<string, string> = {}
|
||||||
|
|
||||||
@ -214,25 +214,27 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
|
|||||||
}
|
}
|
||||||
|
|
||||||
invoiceId = (
|
invoiceId = (
|
||||||
await trx('invoice').insert({ financialYearId: currentYear.id, fiskenNumber, supplierId }).returning('id')
|
await trx('invoice')
|
||||||
|
.insert({ financialYearId: currentYear!.id, fiskenNumber, supplierId })
|
||||||
|
.returning('id')
|
||||||
)[0]?.id
|
)[0]?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transaction.accountNumber === 2441 && currentEntry.description?.includes('Fakturajournal')) {
|
if (transaction.accountNumber === 2441 && currentEntry!.description?.includes('Fakturajournal')) {
|
||||||
await trx('invoice').update('amount', Math.abs(transaction.amount)).where('id', invoiceId)
|
await trx('invoice').update('amount', Math.abs(transaction.amount)).where('id', invoiceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (invoiceId && currentInvoiceId) {
|
if (invoiceId! && currentInvoiceId!) {
|
||||||
throw new Error('invoiceId and currentInvoiceId')
|
throw new Error('invoiceId and currentInvoiceId')
|
||||||
}
|
}
|
||||||
|
|
||||||
const transactionId = (
|
const transactionId = (
|
||||||
await trx('transaction')
|
await trx('transaction')
|
||||||
.insert({
|
.insert({
|
||||||
entryId: currentEntry.id,
|
entryId: currentEntry!.id,
|
||||||
...transaction,
|
...transaction,
|
||||||
invoiceId: invoiceId || currentInvoiceId,
|
invoiceId: invoiceId! || currentInvoiceId!,
|
||||||
})
|
})
|
||||||
.returning('id')
|
.returning('id')
|
||||||
)[0].id
|
)[0].id
|
||||||
@ -269,11 +271,11 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
|
|||||||
|
|
||||||
const existingAccountBalance = await trx('accountBalance')
|
const existingAccountBalance = await trx('accountBalance')
|
||||||
.first('*')
|
.first('*')
|
||||||
.where({ financialYearId: currentYear.id, accountNumber })
|
.where({ financialYearId: currentYear!.id, accountNumber })
|
||||||
|
|
||||||
if (!existingAccountBalance) {
|
if (!existingAccountBalance) {
|
||||||
await trx('accountBalance').insert({
|
await trx('accountBalance').insert({
|
||||||
financialYearId: currentYear.id,
|
financialYearId: currentYear!.id,
|
||||||
accountNumber,
|
accountNumber,
|
||||||
out: balance,
|
out: balance,
|
||||||
outQuantity: quantity,
|
outQuantity: quantity,
|
||||||
@ -285,7 +287,7 @@ export default async function parseStream(stream: ReadableStream, decoder: Decod
|
|||||||
outQuantity: quantity,
|
outQuantity: quantity,
|
||||||
})
|
})
|
||||||
.where({
|
.where({
|
||||||
financialYearId: currentYear.id,
|
financialYearId: currentYear!.id,
|
||||||
accountNumber,
|
accountNumber,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import util from 'node:util'
|
import util from 'node:util'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
// @ts-ignore
|
||||||
import highlightStack from '@bmp/highlight-stack'
|
import highlightStack from '@bmp/highlight-stack'
|
||||||
|
|
||||||
const LEVELS = {
|
const LEVELS: Record<string, string> = {
|
||||||
default: 'USERLVL',
|
default: 'USERLVL',
|
||||||
60: 'FATAL',
|
60: 'FATAL',
|
||||||
50: 'ERROR',
|
50: 'ERROR',
|
||||||
@ -13,7 +14,7 @@ const LEVELS = {
|
|||||||
10: 'TRACE',
|
10: 'TRACE',
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS: Record<string, any> = {
|
||||||
60: chalk.bgRed,
|
60: chalk.bgRed,
|
||||||
50: chalk.red,
|
50: chalk.red,
|
||||||
40: chalk.yellow,
|
40: chalk.yellow,
|
||||||
@ -24,7 +25,7 @@ const COLORS = {
|
|||||||
|
|
||||||
const requests = new Map()
|
const requests = new Map()
|
||||||
|
|
||||||
function colorStatusCode(statusCode) {
|
function colorStatusCode(statusCode: number) {
|
||||||
if (statusCode < 300) {
|
if (statusCode < 300) {
|
||||||
return chalk.bold.green(statusCode)
|
return chalk.bold.green(statusCode)
|
||||||
} else if (statusCode < 400) {
|
} else if (statusCode < 400) {
|
||||||
@ -36,30 +37,56 @@ function colorStatusCode(statusCode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BaseLineObject {
|
||||||
|
level: number
|
||||||
|
time: number
|
||||||
|
pid: number
|
||||||
|
reqId: string
|
||||||
|
msg: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorLineObject extends BaseLineObject {
|
||||||
|
level: 50
|
||||||
|
err: ANY
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IncomingRequestLineObject extends BaseLineObject {
|
||||||
|
msg: 'incoming request'
|
||||||
|
req: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RequestCompletedLineObject extends BaseLineObject {
|
||||||
|
msg: 'request completed'
|
||||||
|
res: Record<string, any>
|
||||||
|
responseTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineObject = ErrorLineObject | IncomingRequestLineObject | RequestCompletedLineObject | BaseLineObject
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
write(line) {
|
write(line: string) {
|
||||||
const obj = JSON.parse(line)
|
const obj: LineObject = JSON.parse(line)
|
||||||
|
|
||||||
if (obj.msg === 'incoming request') {
|
if (obj.msg === 'incoming request') {
|
||||||
requests.set(obj.reqId, obj.req)
|
requests.set(obj.reqId, (obj as IncomingRequestLineObject).req)
|
||||||
} else if (obj.msg === 'request completed') {
|
} else if (obj.msg === 'request completed') {
|
||||||
const req = requests.get(obj.reqId)
|
const req = requests.get(obj.reqId)
|
||||||
requests.delete(obj.reqId)
|
requests.delete(obj.reqId)
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`${chalk.bold(req.method)} ${req.url} ${colorStatusCode(obj.res.statusCode)} ${obj.responseTime.toFixed(
|
`${chalk.bold(req.method)} ${req.url} ${colorStatusCode((obj as RequestCompletedLineObject).res.statusCode)} ${(
|
||||||
3,
|
obj as RequestCompletedLineObject
|
||||||
)} ms\n`,
|
).responseTime.toFixed(3)} ms\n`,
|
||||||
)
|
)
|
||||||
} else if (obj.level === 50) {
|
} else if (obj.level === 50) {
|
||||||
// TODO figure out if there is a way to get the error instances here... console.log(Error) and util.inspect(Error)
|
// TODO figure out if there is a way to get the error instances here... console.log(Error) and util.inspect(Error)
|
||||||
// looks better than the serialized errors
|
// looks better than the serialized errors
|
||||||
if (obj.err.status < 500) return
|
if ((obj as ErrorLineObject).err.status < 500) return
|
||||||
|
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`${COLORS[obj.level](LEVELS[obj.level] || LEVELS.default)} ${highlightStack(obj.err.stack)}\n`,
|
`${COLORS[obj.level](LEVELS[obj.level] || LEVELS.default)} ${highlightStack((obj as ErrorLineObject).err.stack)}\n`,
|
||||||
)
|
)
|
||||||
|
|
||||||
const details = _.omit(obj.err, ['type', 'message', 'stack'])
|
const details = _.omit((obj as ErrorLineObject).err, ['type', 'message', 'stack'])
|
||||||
|
|
||||||
if (!_.isEmpty(details)) {
|
if (!_.isEmpty(details)) {
|
||||||
process.stdout.write(`${chalk.red('Details')} ${util.inspect(details, false, 4, true)}\n`)
|
process.stdout.write(`${chalk.red('Details')} ${util.inspect(details, false, 4, true)}\n`)
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
|
import { TransformStream } from 'stream/web'
|
||||||
|
|
||||||
const defaultMatcher = /\r?\n/
|
const defaultMatcher = /\r?\n/
|
||||||
|
|
||||||
export type Decoder = {
|
export type Decoder = {
|
||||||
decode: (chunk?: ArrayBufferView) => void
|
decode: (chunk: Uint8Array) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Options {
|
interface Options {
|
||||||
decoder?: Decoder
|
decoder?: Decoder
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function split(matcher: RegExp, { decoder }: Options = {}) {
|
export default function split(matcher?: RegExp | null, { decoder }: Options = {}) {
|
||||||
matcher ??= defaultMatcher
|
matcher ??= defaultMatcher
|
||||||
|
|
||||||
let rest: string | null = null
|
let rest: string | null | undefined = null
|
||||||
|
|
||||||
return new TransformStream({
|
return new TransformStream({
|
||||||
start() {},
|
start() {},
|
||||||
|
|||||||
@ -1,35 +1,7 @@
|
|||||||
import { type FastifyPluginCallback } from 'fastify'
|
import { type FastifyPluginCallback } from 'fastify'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import fp from 'fastify-plugin'
|
import fp from 'fastify-plugin'
|
||||||
import { type Template } from '../lib/html.ts'
|
import type { Config, Entry, EntryWithoutName } from './vite/types.ts'
|
||||||
|
|
||||||
export interface Entry {
|
|
||||||
name: string
|
|
||||||
path: string
|
|
||||||
template: (...args: any[]) => Template
|
|
||||||
routes?: ANY[]
|
|
||||||
ssr?: boolean
|
|
||||||
hostname?: string
|
|
||||||
ctx?: Record<string, any>
|
|
||||||
setupRebuildCache?: (buildCache: () => any) => any
|
|
||||||
preHandler?: ANY
|
|
||||||
createPreHandler?: (route: ANY, entry: Entry) => any
|
|
||||||
createErrorHandler?: (handler?: ANY) => ANY
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EntryWithoutName = Omit<Entry, 'name'>
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
mode: 'development' | 'production'
|
|
||||||
createPreHandler?: (route: ANY, entry: Entry) => any
|
|
||||||
createErrorHandler?: (handler?: ANY) => ANY
|
|
||||||
entries: Record<string, EntryWithoutName>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ParsedConfig {
|
|
||||||
mode: 'development' | 'production'
|
|
||||||
entries: Entry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const vitePlugin: FastifyPluginCallback<Config> = async function (fastify, config) {
|
const vitePlugin: FastifyPluginCallback<Config> = async function (fastify, config) {
|
||||||
const parsedConfig = {
|
const parsedConfig = {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { type FastifyInstance } from 'fastify'
|
import type { FastifyInstance, RouteHandler } from 'fastify'
|
||||||
import fmiddie from '@fastify/middie'
|
import fmiddie from '@fastify/middie'
|
||||||
import { createServer } from 'vite'
|
import { createServer } from 'vite'
|
||||||
import StatusError from '../../lib/status_error.ts'
|
import StatusError from '../../lib/status_error.ts'
|
||||||
|
|
||||||
import { type Entry, type ParsedConfig } from '../vite.ts'
|
import type { Route } from '../../../shared/types.ts'
|
||||||
|
import type { Entry, ParsedConfig, Renderer, RenderFunction } from './types.ts'
|
||||||
|
|
||||||
export default async function viteDevelopment(fastify: FastifyInstance, config: ParsedConfig) {
|
export default async function viteDevelopment(fastify: FastifyInstance, config: ParsedConfig) {
|
||||||
const devServer = await createServer({
|
const devServer = await createServer({
|
||||||
@ -40,7 +41,7 @@ async function setupEntry(fastify: FastifyInstance, entry: Entry) {
|
|||||||
if (entry.preHandler) {
|
if (entry.preHandler) {
|
||||||
fastify.addHook('preHandler', entry.preHandler)
|
fastify.addHook('preHandler', entry.preHandler)
|
||||||
}
|
}
|
||||||
const routes = entry.routes || (await fastify.devServer.ssrLoadModule(`/${entry.name}/server.ts`)).routes
|
const routes: Route[] = entry.routes || (await fastify.devServer.ssrLoadModule(`/${entry.name}/server.ts`)).routes
|
||||||
|
|
||||||
for (const route of routes.flatMap((route) => route.routes || route)) {
|
for (const route of routes.flatMap((route) => route.routes || route)) {
|
||||||
// const preHandler = entry.preHandler || entry.createPreHandler?.(route, entry)
|
// const preHandler = entry.preHandler || entry.createPreHandler?.(route, entry)
|
||||||
@ -79,28 +80,32 @@ async function setupEntry(fastify: FastifyInstance, entry: Entry) {
|
|||||||
throw new StatusError(404)
|
throw new StatusError(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.setErrorHandler(
|
if (entry.createErrorHandler) {
|
||||||
entry.createErrorHandler(async (error, request, reply) => {
|
fastify.setErrorHandler(
|
||||||
reply.type('text/html').status(error.status || 500)
|
entry.createErrorHandler(async (error, request, reply) => {
|
||||||
|
reply.type('text/html').status((error as StatusError).status || 500)
|
||||||
|
|
||||||
return renderer(
|
return renderer(
|
||||||
request.url,
|
request.url,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
error,
|
error,
|
||||||
},
|
},
|
||||||
reply.ctx,
|
reply.ctx,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRenderer(fastify, entry) {
|
function createRenderer(fastify: FastifyInstance, entry: Entry): Renderer {
|
||||||
return async (url, ctx) => {
|
return async (url, ctx) => {
|
||||||
ctx = Object.assign({ url }, entry.ctx, ctx)
|
ctx = Object.assign({ url }, entry.ctx, ctx)
|
||||||
|
|
||||||
const { render } = await fastify.devServer.ssrLoadModule(`/${entry.name}/server.ts`)
|
const { render } = (await fastify.devServer.ssrLoadModule(`/${entry.name}/server.ts`)) as {
|
||||||
|
render?: RenderFunction
|
||||||
|
}
|
||||||
|
|
||||||
const renderPromise = render && entry.ssr !== false ? render(ctx) : null
|
const renderPromise = render && entry.ssr !== false ? render(ctx) : null
|
||||||
|
|
||||||
@ -121,7 +126,7 @@ function createRenderer(fastify, entry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHandler(renderer) {
|
function createHandler(renderer: Renderer): RouteHandler {
|
||||||
return async function handler(request, reply) {
|
return async function handler(request, reply) {
|
||||||
reply.type('text/html')
|
reply.type('text/html')
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
import { type FastifyInstance } from 'fastify'
|
|
||||||
|
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
|
import { type Readable } from 'node:stream'
|
||||||
|
import { resolveConfig, type ResolvedConfig as ViteResolvedConfig, type Manifest as ViteManifest } from 'vite'
|
||||||
|
import type { FastifyInstance, RouteHandler } from 'fastify'
|
||||||
import fstatic from '@fastify/static'
|
import fstatic from '@fastify/static'
|
||||||
import { resolveConfig } from 'vite'
|
|
||||||
import StatusError from '../../lib/status_error.ts'
|
import StatusError from '../../lib/status_error.ts'
|
||||||
|
|
||||||
import { type Entry, type ParsedConfig } from '../vite.ts'
|
import type { Route } from '../../../shared/types.ts'
|
||||||
|
import type { Entry, ParsedConfig, Renderer, RenderCache, RenderFunction, ViteEntryFile } from './types.ts'
|
||||||
|
|
||||||
export default async function viteProduction(fastify: FastifyInstance, config: ParsedConfig) {
|
export default async function viteProduction(fastify: FastifyInstance, config: ParsedConfig) {
|
||||||
const viteConfig = await resolveConfig({}, 'build', 'production')
|
const viteConfig = await resolveConfig({}, 'build', 'production')
|
||||||
@ -25,18 +27,27 @@ export default async function viteProduction(fastify: FastifyInstance, config: P
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupEntry(fastify: FastifyInstance, entry: Entry, config: ParsedConfig, viteConfig) {
|
async function setupEntry(
|
||||||
const viteFilePath = path.join(viteConfig.build.outDir, entry.name + '.js')
|
fastify: FastifyInstance,
|
||||||
const viteFile = fs.existsSync(viteFilePath)
|
entry: Entry,
|
||||||
? await import(viteFilePath)
|
_config: ParsedConfig,
|
||||||
|
viteConfig: ViteResolvedConfig,
|
||||||
|
) {
|
||||||
|
const viteEntryFilePath = path.join(viteConfig.build.outDir, entry.name + '.js')
|
||||||
|
const viteEntryFile: ViteEntryFile = fs.existsSync(viteEntryFilePath)
|
||||||
|
? await import(viteEntryFilePath)
|
||||||
: await import(path.join(viteConfig.root, entry.name, 'server.ts'))
|
: await import(path.join(viteConfig.root, entry.name, 'server.ts'))
|
||||||
const manifestPath = path.join(viteConfig.build.outDir, `manifest.${entry.name}.json`)
|
const manifestPath = path.join(viteConfig.build.outDir, `manifest.${entry.name}.json`)
|
||||||
const manifest = fs.existsSync(manifestPath) ? (await import(manifestPath, { with: { type: 'json' } })).default : null
|
const manifest: ViteManifest = fs.existsSync(manifestPath)
|
||||||
const routes = entry.routes || viteFile.routes
|
? (await import(manifestPath, { with: { type: 'json' } })).default
|
||||||
|
: null
|
||||||
|
const routes = entry.routes || viteEntryFile.routes
|
||||||
|
|
||||||
|
if (!routes) throw new Error('No routes found')
|
||||||
|
|
||||||
const cache = new Map()
|
const cache = new Map()
|
||||||
|
|
||||||
const renderer = createRenderer(fastify, entry, viteFile.render, manifest)
|
const renderer = createRenderer(fastify, entry, viteEntryFile.render, manifest)
|
||||||
const cachedHandler = createCachedHandler(cache)
|
const cachedHandler = createCachedHandler(cache)
|
||||||
const handler = createHandler(renderer)
|
const handler = createHandler(renderer)
|
||||||
|
|
||||||
@ -90,24 +101,31 @@ async function setupEntry(fastify: FastifyInstance, entry: Entry, config: Parsed
|
|||||||
throw new StatusError(404)
|
throw new StatusError(404)
|
||||||
})
|
})
|
||||||
|
|
||||||
fastify.setErrorHandler(
|
if (entry.createErrorHandler) {
|
||||||
entry.createErrorHandler(async (error, request, reply) => {
|
fastify.setErrorHandler(
|
||||||
reply.type('text/html').status(error.status || 500)
|
entry.createErrorHandler(async (error, request, reply) => {
|
||||||
|
reply.type('text/html').status((error as StatusError).status || 500)
|
||||||
|
|
||||||
return renderer(
|
return renderer(
|
||||||
request.url,
|
request.url,
|
||||||
Object.assign(
|
Object.assign(
|
||||||
{
|
{
|
||||||
error,
|
error,
|
||||||
},
|
},
|
||||||
reply.ctx,
|
reply.ctx,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRenderer(fastify, entry, render, manifest) {
|
function createRenderer(
|
||||||
|
_fastify: FastifyInstance,
|
||||||
|
entry: Entry,
|
||||||
|
render: RenderFunction | null | undefined,
|
||||||
|
manifest: ViteManifest,
|
||||||
|
): Renderer {
|
||||||
const files = manifest[`${entry.name}/client.ts`]
|
const files = manifest[`${entry.name}/client.ts`]
|
||||||
const bundle = path.join('/', files.file)
|
const bundle = path.join('/', files.file)
|
||||||
const preload = files.imports?.map((name) => path.join('/', manifest[name].file))
|
const preload = files.imports?.map((name) => path.join('/', manifest[name].file))
|
||||||
@ -124,7 +142,7 @@ function createRenderer(fastify, entry, render, manifest) {
|
|||||||
script: bundle,
|
script: bundle,
|
||||||
preload,
|
preload,
|
||||||
css,
|
css,
|
||||||
content: renderPromise?.then((result) => result.content),
|
content: renderPromise?.then((result) => result.html),
|
||||||
head: renderPromise?.then((result) => result.head),
|
head: renderPromise?.then((result) => result.head),
|
||||||
state: renderPromise?.then((result) => result.state) || Promise.resolve(ctx),
|
state: renderPromise?.then((result) => result.state) || Promise.resolve(ctx),
|
||||||
})
|
})
|
||||||
@ -132,7 +150,7 @@ function createRenderer(fastify, entry, render, manifest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHandler(renderer) {
|
function createHandler(renderer: Renderer): RouteHandler {
|
||||||
return async function handler(request, reply) {
|
return async function handler(request, reply) {
|
||||||
reply.type('text/html')
|
reply.type('text/html')
|
||||||
|
|
||||||
@ -140,7 +158,7 @@ function createHandler(renderer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCachedHandler(cache) {
|
function createCachedHandler(cache: RenderCache): RouteHandler {
|
||||||
return function cachedHandler(request, reply) {
|
return function cachedHandler(request, reply) {
|
||||||
reply.type('text/html')
|
reply.type('text/html')
|
||||||
|
|
||||||
@ -148,13 +166,13 @@ function createCachedHandler(cache) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildCache(entry, routes, renderer, cache) {
|
async function buildCache(_entry: Entry, routes: Route[], renderer: Renderer, cache: RenderCache) {
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (route.cache) {
|
if (route.cache) {
|
||||||
let html = ''
|
let html = ''
|
||||||
|
|
||||||
// TODO run preHandlers
|
// TODO run preHandlers
|
||||||
for await (const chunk of renderer(route.path)) {
|
for await (const chunk of renderer(route.path) as Readable) {
|
||||||
html += chunk
|
html += chunk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
server/plugins/vite/types.ts
Normal file
56
server/plugins/vite/types.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import type { Readable } from 'node:stream'
|
||||||
|
import type { preHandlerHookHandler } from 'fastify'
|
||||||
|
import type { Template } from '../../lib/html.ts'
|
||||||
|
import type { ErrorHandler } from '../../types.ts'
|
||||||
|
import type { Route } from '../../../shared/types.ts'
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
mode: 'development' | 'production'
|
||||||
|
createPreHandler?: (route: Route, entry: Entry) => preHandlerHookHandler
|
||||||
|
createErrorHandler?: (handler?: ErrorHandler) => ErrorHandler
|
||||||
|
entries: Record<string, EntryWithoutName>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedConfig {
|
||||||
|
mode: 'development' | 'production'
|
||||||
|
entries: Entry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Entry {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
template: (...args: any[]) => Template
|
||||||
|
routes?: Route[]
|
||||||
|
ssr?: boolean
|
||||||
|
hostname?: string
|
||||||
|
ctx?: Record<string, any>
|
||||||
|
setupRebuildCache?: (buildCache: () => any) => any
|
||||||
|
preHandler?: preHandlerHookHandler
|
||||||
|
createPreHandler?: (route: Route, entry: Entry) => preHandlerHookHandler
|
||||||
|
createErrorHandler?: (handler?: ErrorHandler) => ErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntryWithoutName = Omit<Entry, 'name'>
|
||||||
|
|
||||||
|
export type RenderCache = Map<string, string>
|
||||||
|
|
||||||
|
export interface RenderFunctionResult {
|
||||||
|
html?: string
|
||||||
|
head: {
|
||||||
|
title?: string
|
||||||
|
tags?: {
|
||||||
|
type: string
|
||||||
|
tags: Record<string, string>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RenderFunction = (ctx: ANY) => Promise<RenderFunctionResult>
|
||||||
|
|
||||||
|
export type Renderer = (url: string, ctx?: ANY) => string | Promise<string> | Readable
|
||||||
|
|
||||||
|
export interface ViteEntryFile {
|
||||||
|
routes?: Route[]
|
||||||
|
render?: RenderFunction
|
||||||
|
}
|
||||||
@ -1,6 +1,21 @@
|
|||||||
import html from '../lib/html.ts'
|
import html from '../lib/html.ts'
|
||||||
|
|
||||||
export default ({ content, css, head, preload, script, state }) => html`<!DOCTYPE html>
|
interface Options {
|
||||||
|
content: string
|
||||||
|
css: string[]
|
||||||
|
head: Promise<{
|
||||||
|
title?: string
|
||||||
|
tags?: {
|
||||||
|
type: string
|
||||||
|
attributes: Record<string, string>
|
||||||
|
}[]
|
||||||
|
}>
|
||||||
|
preload?: string[]
|
||||||
|
script: string
|
||||||
|
state: Promise<Record<string, any>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ content, css, head, preload, script, state }: Options) => html`<!DOCTYPE html>
|
||||||
<html lang='sv-SE'>
|
<html lang='sv-SE'>
|
||||||
<head>
|
<head>
|
||||||
<link rel="icon" href="/favicon.svg" />
|
<link rel="icon" href="/favicon.svg" />
|
||||||
|
|||||||
10
server/types.ts
Normal file
10
server/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { type FastifyRequest, type FastifyReply } from 'fastify'
|
||||||
|
|
||||||
|
import { type DatabaseError } from 'pg-protocol'
|
||||||
|
import type StatusError from './lib/status_error.ts'
|
||||||
|
|
||||||
|
export type ErrorHandler = (
|
||||||
|
error: Error | DatabaseError | StatusError,
|
||||||
|
request: FastifyRequest,
|
||||||
|
reply: FastifyReply,
|
||||||
|
) => any | Promise<any>
|
||||||
2
shared/global.d.ts
vendored
2
shared/global.d.ts
vendored
@ -20,7 +20,7 @@ declare module 'fastify' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FastifyReply {
|
interface FastifyReply {
|
||||||
ctx: { [key: string]: any }
|
ctx: Record<string, any> | null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
// export const FinancialYear = Type.Object({
|
export type Account = {
|
||||||
// year: Type.Number(),
|
id: number
|
||||||
// startDate: Type.String(),
|
number: number
|
||||||
// endDate: Type.String(),
|
|
||||||
// })
|
|
||||||
|
|
||||||
// export type FinancialYearType = Static<typeof FinancialYear>
|
|
||||||
export interface Transaction {
|
|
||||||
description: string
|
description: string
|
||||||
amount: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Balance = {
|
||||||
|
accountNumber: string
|
||||||
|
description: string
|
||||||
|
} & Record<number, number>
|
||||||
|
|
||||||
export interface Entry {
|
export interface Entry {
|
||||||
id: number
|
id: number
|
||||||
journal: string
|
journal: string
|
||||||
@ -24,3 +23,71 @@ export interface Entry {
|
|||||||
amount: number
|
amount: number
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FinancialYear = {
|
||||||
|
year: number
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Invoice = {
|
||||||
|
id: number
|
||||||
|
fiskenNumber?: number
|
||||||
|
phmNumber?: number
|
||||||
|
invoiceDate: string
|
||||||
|
dueDate: string
|
||||||
|
invoiceNumber: number
|
||||||
|
amount: number
|
||||||
|
files?: { filename: string }[]
|
||||||
|
transactions?: {
|
||||||
|
account_number: number
|
||||||
|
amount: number
|
||||||
|
description: number
|
||||||
|
entry_id: number
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Journal = {
|
||||||
|
id: number
|
||||||
|
identifier: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Object = {
|
||||||
|
id: number
|
||||||
|
dimensionName: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Result = {
|
||||||
|
accountNumber: number
|
||||||
|
description?: string
|
||||||
|
} & Record<number, number>
|
||||||
|
|
||||||
|
export type Supplier = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Transaction {
|
||||||
|
accountNumber: number
|
||||||
|
description: string
|
||||||
|
amount: number
|
||||||
|
entryId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionFull extends Transaction {
|
||||||
|
transactionDate: string
|
||||||
|
invoiceId: number
|
||||||
|
entryDescription: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Route {
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
title: string
|
||||||
|
component: (args: ANY) => ANY
|
||||||
|
cache?: boolean
|
||||||
|
nav?: boolean
|
||||||
|
routes?: Route[]
|
||||||
|
locales?: ANY[]
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user