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