Compare commits

...

36 Commits

Author SHA1 Message Date
Katja Lutz 1119f7b0b1 chore(release): 1.3.0 2 years ago
Katja Lutz be3d1888b3 fix: check for proper error message to silent common typer-js resume bug 2 years ago
Katja Lutz 6c801637f6 feat: implement like button
closes issue #18
2 years ago
Katja Lutz 153a86ab59 chore: implement server onExit utility 2 years ago
Katja Lutz f4dd2cdb7f feat: reset lastSaved time after document load 2 years ago
Katja Lutz 2ffdc401f1 feat: implement logo delete button 2 years ago
Katja Lutz f54b309b65 chore: support jsx element as TextInput suffix 2 years ago
Katja Lutz 24e82ace96 feat: implement addressLayout setting and optimize address layouts for print
closes issue #14
2 years ago
Katja Lutz c9328257db feat: move corporate design section to design accordion item 2 years ago
Katja Lutz 12c9ffb935 feat: change Select input-group size from small to normal 2 years ago
Katja Lutz 72943cc41d feat: convert Select input-group to label 2 years ago
Katja Lutz 514d06b13c chore: improve defaultPositionType select title 2 years ago
Katja Lutz 058c973e15 refactor: extract defaultPositionType select into new Select component 2 years ago
Katja Lutz 4673ba8019 feat: disable word wrapping for invoice position prices and quantity 2 years ago
Katja Lutz 0b55a50d76 feat: preview fullWidthInvoice setting closer to reality 2 years ago
Katja Lutz d90d0b2f8c chore(locale): change word Pixel to Pixeln in WelcomeModal 2 years ago
Katja Lutz 23b3412379 feat: play trailer video in WelcomeModal 2 years ago
Katja Lutz 0462e70229 refactor: replace own shuffle function with external froebel shuffle 2 years ago
Katja Lutz b5530130ef chore(deps): update dependencies 2 years ago
Katja Lutz 994e5d8163 feat: collapse overlay on small devices 2 years ago
Katja Lutz d294c100c6 feat: improve WelcomeModal table of contents layout 2 years ago
Katja Lutz d99c0d6bc2 feat: reduce top margin for WelcomeModal footer on small devices 2 years ago
Katja Lutz b54bb7c1b9 feat: make unsupported devices warning more obvious 2 years ago
Katja Lutz 39a72b6670 fix: in print preview show same page margin independent of fullWidthInvoice 2 years ago
Katja Lutz d04ad556d1 feat: add support for screens with min-width 1024px 2 years ago
Katja Lutz 21017278f2 chore(release): 1.2.0 2 years ago
Katja Lutz cf2a1c4d90 feat: implement custom NumberInput component 2 years ago
Katja Lutz a935e6fe56 feat: support decimals in position quantity input
closes issue #12
2 years ago
Katja Lutz d5a4a739a6 chore: show warning if vatRate is over 100
related issue #11
2 years ago
Katja Lutz 0bb8423b01 feat: support non-numeric characters in line2 street field
closes issue #8
2 years ago
Katja Lutz 6b89e28c5f feat: replace gray with slate bg color for form labels
closes issue #3
2 years ago
Katja Lutz af7511e459 chore(release): 1.1.0 2 years ago
Katja Lutz d20a44990b feat: set maxlength 50 for iban input
closes issue #5
2 years ago
Katja Lutz 95784540c9 feat: implement error handling for QrCode generation
closes issue #4
2 years ago
Katja Lutz 11739dc70c chore(release): 1.0.1 2 years ago
Katja Lutz 82b9ae551a fix: correct typo in label for useCustomAddress input 2 years ago

1
.gitignore vendored

@ -5,6 +5,7 @@ dist
.vercel
.netlify
netlify
/data.json
# dependencies
/node_modules

@ -1,3 +1,5 @@
# Storage
/data.json
# IDEs and editors
/.idea

@ -2,6 +2,58 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [1.3.0](https://git.lufrai.com/rappli/rappli/compare/v1.2.0...v1.3.0) (2022-07-28)
### Features
* add support for screens with min-width 1024px ([d04ad55](https://git.lufrai.com/rappli/rappli/commit/d04ad556d1d30e1760b01a5e9c12beff7f0b4149))
* change Select input-group size from small to normal ([12c9ffb](https://git.lufrai.com/rappli/rappli/commit/12c9ffb93519ff9b8fa785541344dc51e225614d))
* collapse overlay on small devices ([994e5d8](https://git.lufrai.com/rappli/rappli/commit/994e5d81634db5b960d3e80f15af5d0907b6fc6d))
* convert Select input-group to label ([72943cc](https://git.lufrai.com/rappli/rappli/commit/72943cc41d5650db987f058c520a8cc410c0ee15))
* disable word wrapping for invoice position prices and quantity ([4673ba8](https://git.lufrai.com/rappli/rappli/commit/4673ba80194a7934a9fecb85642c31f2986af54a))
* implement addressLayout setting and optimize address layouts for print ([24e82ac](https://git.lufrai.com/rappli/rappli/commit/24e82ace965c2a8c8946f2ce5fc4662d55b98b43)), closes [#14](https://git.lufrai.com/rappli/rappli/issues/14)
* implement like button ([6c80163](https://git.lufrai.com/rappli/rappli/commit/6c801637f6a3131ec10892662c9269a1130714c8)), closes [#18](https://git.lufrai.com/rappli/rappli/issues/18)
* implement logo delete button ([2ffdc40](https://git.lufrai.com/rappli/rappli/commit/2ffdc401f1c432871d7229ac84df0ad238a18585))
* improve WelcomeModal table of contents layout ([d294c10](https://git.lufrai.com/rappli/rappli/commit/d294c100c6d8d430aa12ef05d3880950269fa951))
* make unsupported devices warning more obvious ([b54bb7c](https://git.lufrai.com/rappli/rappli/commit/b54bb7c1b912e2096f93ce03d70c5912c8023dce))
* move corporate design section to design accordion item ([c932825](https://git.lufrai.com/rappli/rappli/commit/c9328257dbd716b01249a4c362a39668c6236c08))
* play trailer video in WelcomeModal ([23b3412](https://git.lufrai.com/rappli/rappli/commit/23b3412379340db9dd8f9b5acfd32ee3afa557d3))
* preview fullWidthInvoice setting closer to reality ([0b55a50](https://git.lufrai.com/rappli/rappli/commit/0b55a50d763bd540f8062688e837cea3b2616665))
* reduce top margin for WelcomeModal footer on small devices ([d99c0d6](https://git.lufrai.com/rappli/rappli/commit/d99c0d6bc28d3c3c63fa23fdd7f836ae277b126c))
* reset lastSaved time after document load ([f4dd2cd](https://git.lufrai.com/rappli/rappli/commit/f4dd2cdb7f87be4c20167268171947673224fb80))
### Bug Fixes
* check for proper error message to silent common typer-js resume bug ([be3d188](https://git.lufrai.com/rappli/rappli/commit/be3d1888b361c8a182ccfaedf10757cea1b30a49))
* in print preview show same page margin independent of fullWidthInvoice ([39a72b6](https://git.lufrai.com/rappli/rappli/commit/39a72b6670b31ecf3c2dee255d087765e931a604))
## [1.2.0](https://git.lufrai.com/rappli/rappli/compare/v1.1.0...v1.2.0) (2022-07-13)
### Features
* implement custom NumberInput component ([cf2a1c4](https://git.lufrai.com/rappli/rappli/commit/cf2a1c4d9040cd4d3a3a317c8bca14c032f5167e))
* replace gray with slate bg color for form labels ([6b89e28](https://git.lufrai.com/rappli/rappli/commit/6b89e28c5f98139a5deee5b4c1786c4d5c1935d8)), closes [#3](https://git.lufrai.com/rappli/rappli/issues/3)
* support decimals in position quantity input ([a935e6f](https://git.lufrai.com/rappli/rappli/commit/a935e6fe563ca9ada14c9dd0ff28551a5e1f69aa)), closes [#12](https://git.lufrai.com/rappli/rappli/issues/12)
* support non-numeric characters in line2 street field ([0bb8423](https://git.lufrai.com/rappli/rappli/commit/0bb8423b01a26d3f96ae5869483611d086fb2606)), closes [#8](https://git.lufrai.com/rappli/rappli/issues/8)
## [1.1.0](https://git.lufrai.com/rappli/rappli/compare/v1.0.1...v1.1.0) (2022-07-07)
### Features
* implement error handling for QrCode generation ([9578454](https://git.lufrai.com/rappli/rappli/commit/95784540c9d447e0d35e82dbfea0d5034fd5a543)), closes [#4](https://git.lufrai.com/rappli/rappli/issues/4)
* set maxlength 50 for iban input ([d20a449](https://git.lufrai.com/rappli/rappli/commit/d20a44990bcdf5ca23bc88959504efa24ded1f7b)), closes [#5](https://git.lufrai.com/rappli/rappli/issues/5)
### [1.0.1](https://git.lufrai.com/rappli/rappli/compare/v1.0.0...v1.0.1) (2022-07-04)
### Bug Fixes
* correct typo in label for useCustomAddress input ([82b9ae5](https://git.lufrai.com/rappli/rappli/commit/82b9ae551aa5432f2af9e62e82960561163abc01))
## [1.0.0](https://git.lufrai.com/rappli/rappli/compare/v0.4.0...v1.0.0) (2022-07-01)

259
package-lock.json generated

@ -1,41 +1,41 @@
{
"name": "rappli",
"version": "1.0.0",
"version": "1.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rappli",
"version": "1.0.0",
"version": "1.3.0",
"license": "MIT",
"dependencies": {
"solid-start": "v0.1.0-alpha.89",
"solid-start-node": "next",
"unplugin-icons": "^0.14.6",
"vite": "^2.9.13"
"unplugin-icons": "^0.14.7",
"vite": "^2.9.14"
},
"bin": {
"rappli": "bin/rappli.js"
},
"devDependencies": {
"@formkit/auto-animate": "^1.0.0-beta.1",
"@iconify/json": "^2.1.69",
"@tailwindcss/typography": "^0.5.2",
"@types/big.js": "^6.1.4",
"@iconify/json": "^2.1.81",
"@tailwindcss/typography": "^0.5.4",
"@types/big.js": "^6.1.5",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1.13.0",
"autoprefixer": "^10.4.2",
"autosize": "^5.0.1",
"big.js": "^6.2.0",
"daisyui": "^2.17.0",
"date-fns": "^2.28.0",
"froebel": "^0.17.0",
"big.js": "^6.2.1",
"daisyui": "^2.20.0",
"date-fns": "^2.29.1",
"froebel": "^0.18.0",
"myzod": "^1.8.7",
"nanoid": "^4.0.0",
"node-iso11649": "^2.1.2",
"postcss": "^8.4.14",
"prettier": "^2.7.1",
"qrcode": "^1.5.0",
"qrcode": "^1.5.1",
"rehype-stringify": "^9.0.3",
"remark": "^14.0.2",
"remark-breaks": "^3.0.2",
@ -44,13 +44,14 @@
"remark-rehype": "^10.1.0",
"shareon": "^2.0.0",
"solid-app-router": "^0.4.1",
"solid-js": "^1.4.5",
"solid-confetti-explosion": "^1.0.3",
"solid-js": "^1.4.8",
"solid-meta": "^0.27.5",
"sortablejs": "^1.15.0",
"standard-version": "^9.5.0",
"tailwindcss": "^3.1.4",
"tailwindcss": "^3.1.6",
"typer-js": "^5.5.3",
"typescript": "^4.6.2",
"typescript": "^4.7.4",
"unified": "^10.1.2"
},
"engines": {
@ -1648,9 +1649,9 @@
}
},
"node_modules/@iconify/json": {
"version": "2.1.69",
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.1.69.tgz",
"integrity": "sha512-IJ7InhhWNXfg9apEyDTlGK1Coh5zvSpGStnoOJf36m4Cstqd45qfbObgOevxtUW7zknHlOgMWtw/a9MUNVs1zw==",
"version": "2.1.81",
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.1.81.tgz",
"integrity": "sha512-VF2iPSwKJ5WnQFQjzik9s0bJiAa/qhyQIJuvonG2+ZtUK8NVK7v0UmvSNvZQv8Nt//df6p3tZFZTTD2eRRVi5Q==",
"dev": true,
"dependencies": {
"@iconify/types": "*",
@ -1826,9 +1827,9 @@
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz",
"integrity": "sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.4.tgz",
"integrity": "sha512-QEdg40EmGvE7kKoDei8zr5sf4D1pIayHj4R31bH3lX8x2BtTiR+jNejYPOkhbmy3DXgkMF9jC8xqNiGFAuL9Sg==",
"dev": true,
"dependencies": {
"lodash.castarray": "^4.4.0",
@ -1836,13 +1837,13 @@
"lodash.merge": "^4.6.2"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || insiders"
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/@types/big.js": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.1.4.tgz",
"integrity": "sha512-yBEfFBLpvnIS9MLcKG4XIpBhMRjDWEf4cSJ/uR5cPq2JrYB+yhYMPK8cF17hGfWmsWL7LuzJF/N4APRtmcg4sw==",
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.1.5.tgz",
"integrity": "sha512-UiWyJ6TLWoHeHZ8VUyngzCOwJDVxTsPnqfAMR/85X93rkRk5A4T2U42BCx0wCmZdtMHGHN/utJ8ft5xWu0V1bA==",
"dev": true
},
"node_modules/@types/debug": {
@ -2180,9 +2181,9 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/big.js": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.0.tgz",
"integrity": "sha512-paIKvJiAaOYdLt6MfnvxkDo64lTOV257XYJyX3oJnJQocIclUn+48k6ZerH/c5FxWE6DGJu1TKDYis7tqHg9kg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz",
"integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==",
"dev": true,
"engines": {
"node": "*"
@ -2926,16 +2927,23 @@
"node": ">=4"
}
},
"node_modules/csstype": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==",
"dev": true,
"peer": true
},
"node_modules/daisyui": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.17.0.tgz",
"integrity": "sha512-U8eA8IvwH0JSi72WbRZdmJcYZek3ERMPFPAVFhnYc6dGnobeIHLGBz8J4q012I9YVDUqXFrWz13rpn+SBdn7qQ==",
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.20.0.tgz",
"integrity": "sha512-97xFBWQOXV/JCVRrGI5xranElKMmXIajDxojTQ1qoYwcWlkfFE7hY4D/NyHVIzlQb+B4xbiXhVPJM7EleRAY8g==",
"dev": true,
"dependencies": {
"color": "^4.2",
"css-selector-tokenizer": "^0.8.0",
"postcss-js": "^4.0.0",
"tailwindcss": "^3.0"
"tailwindcss": "^3"
},
"peerDependencies": {
"autoprefixer": "^10.0.2",
@ -2952,9 +2960,9 @@
}
},
"node_modules/date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz",
"integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==",
"dev": true,
"engines": {
"node": ">=0.11"
@ -3778,9 +3786,9 @@
}
},
"node_modules/froebel": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/froebel/-/froebel-0.17.0.tgz",
"integrity": "sha512-reZdfnUIXBF6a1s6mcwx+DctPRZYGmNcU5kUAq47yrQ4rvpSKw/yN4eCIs4uEFLHUpT7bSBnqd60jC2jN7fbGg==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/froebel/-/froebel-0.18.0.tgz",
"integrity": "sha512-dPkcmnYFA58qLY29MExdUg6/DopBfSeltd66oR9MosezkkXOdjHt0qvNPQbsaAdavJkzqEusiWxOa5TvFjMo5g==",
"dev": true
},
"node_modules/fs.realpath": {
@ -4073,6 +4081,16 @@
"node": ">=4"
}
},
"node_modules/goober": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.10.tgz",
"integrity": "sha512-7PpuQMH10jaTWm33sQgBQvz45pHR8N4l3Cu3WMGEWmHShAcTuuP7I+5/DwKo39fwti5A80WAjvqgz6SSlgWmGA==",
"dev": true,
"peer": true,
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@ -6338,9 +6356,9 @@
}
},
"node_modules/qrcode": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz",
"integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
"integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==",
"dev": true,
"dependencies": {
"dijkstrajs": "^1.0.1",
@ -6932,10 +6950,20 @@
"solid-js": "^1.3.5"
}
},
"node_modules/solid-confetti-explosion": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/solid-confetti-explosion/-/solid-confetti-explosion-1.0.3.tgz",
"integrity": "sha512-rAxyxcyhkb7ZCO9fU6YAbH65YH/4+4sCAyaGasd4WzX7XZEpV4bO+VYGYNDV78e/RDwB/8b+PWH25fqwI/igHw==",
"dev": true,
"peerDependencies": {
"solid-js": "^1.4.5",
"solid-styled-components": "^0.28.4"
}
},
"node_modules/solid-js": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.5.tgz",
"integrity": "sha512-32NGpuabEJDTeQ7fjaTR2TLC7R/X5hbqhYdEQ1e+GcIK8r8+/V0Nv17eZQii5Z/97/mtdt8yi63chzg73qnz/A=="
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.8.tgz",
"integrity": "sha512-XErZdnnYYXF7OwGSUAPcua2y5/ELB/c53zFCpWiEGqxTNoH1iQghzI8EsHJXk06sNn+Z/TGhb8bPDNNGSgimag=="
},
"node_modules/solid-meta": {
"version": "0.27.5",
@ -7020,6 +7048,20 @@
"vite": "*"
}
},
"node_modules/solid-styled-components": {
"version": "0.28.4",
"resolved": "https://registry.npmjs.org/solid-styled-components/-/solid-styled-components-0.28.4.tgz",
"integrity": "sha512-SGbXv5tLIs1qErr3x7M+HWE4lu+37C4myV8gbce7WnZumjBmM5sifKv/NulVeSf3nMRa3uwkAM14q7QmLGC2gQ==",
"dev": true,
"peer": true,
"dependencies": {
"csstype": "^3.1.0",
"goober": "^2.1.10"
},
"peerDependencies": {
"solid-js": "^1.4.4"
}
},
"node_modules/sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
@ -7368,9 +7410,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz",
"integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==",
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.6.tgz",
"integrity": "sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==",
"dev": true,
"dependencies": {
"arg": "^5.0.2",
@ -7394,7 +7436,7 @@
"postcss-selector-parser": "^6.0.10",
"postcss-value-parser": "^4.2.0",
"quick-lru": "^5.1.1",
"resolve": "^1.22.0"
"resolve": "^1.22.1"
},
"bin": {
"tailwind": "lib/cli.js",
@ -7751,9 +7793,9 @@
}
},
"node_modules/unplugin-icons": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.6.tgz",
"integrity": "sha512-8sxDiL4l+TV4zufZfrskgHZZSDFoGOCBgYsefRMM4inQ3Z6KhgMSuNyew7U7D/xG//rwxgD7bN+Dv+YAZEEfEw==",
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.7.tgz",
"integrity": "sha512-TrNnEdpaXMdiG5BsCgvU6cv/gSLYvIk1f8wGCGZmOo4wmi3nqYBuqIEuiXhmmyXdDZuRRpCaOzCnCYYZ5H7U8g==",
"dependencies": {
"@antfu/install-pkg": "^0.1.0",
"@antfu/utils": "^0.5.2",
@ -7879,9 +7921,9 @@
}
},
"node_modules/vite": {
"version": "2.9.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.13.tgz",
"integrity": "sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==",
"version": "2.9.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.14.tgz",
"integrity": "sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw==",
"dependencies": {
"esbuild": "^0.14.27",
"postcss": "^8.4.13",
@ -9260,9 +9302,9 @@
"dev": true
},
"@iconify/json": {
"version": "2.1.69",
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.1.69.tgz",
"integrity": "sha512-IJ7InhhWNXfg9apEyDTlGK1Coh5zvSpGStnoOJf36m4Cstqd45qfbObgOevxtUW7zknHlOgMWtw/a9MUNVs1zw==",
"version": "2.1.81",
"resolved": "https://registry.npmjs.org/@iconify/json/-/json-2.1.81.tgz",
"integrity": "sha512-VF2iPSwKJ5WnQFQjzik9s0bJiAa/qhyQIJuvonG2+ZtUK8NVK7v0UmvSNvZQv8Nt//df6p3tZFZTTD2eRRVi5Q==",
"dev": true,
"requires": {
"@iconify/types": "*",
@ -9401,9 +9443,9 @@
}
},
"@tailwindcss/typography": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.2.tgz",
"integrity": "sha512-coq8DBABRPFcVhVIk6IbKyyHUt7YTEC/C992tatFB+yEx5WGBQrCgsSFjxHUr8AWXphWckadVJbominEduYBqw==",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.4.tgz",
"integrity": "sha512-QEdg40EmGvE7kKoDei8zr5sf4D1pIayHj4R31bH3lX8x2BtTiR+jNejYPOkhbmy3DXgkMF9jC8xqNiGFAuL9Sg==",
"dev": true,
"requires": {
"lodash.castarray": "^4.4.0",
@ -9412,9 +9454,9 @@
}
},
"@types/big.js": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.1.4.tgz",
"integrity": "sha512-yBEfFBLpvnIS9MLcKG4XIpBhMRjDWEf4cSJ/uR5cPq2JrYB+yhYMPK8cF17hGfWmsWL7LuzJF/N4APRtmcg4sw==",
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.1.5.tgz",
"integrity": "sha512-UiWyJ6TLWoHeHZ8VUyngzCOwJDVxTsPnqfAMR/85X93rkRk5A4T2U42BCx0wCmZdtMHGHN/utJ8ft5xWu0V1bA==",
"dev": true
},
"@types/debug": {
@ -9697,9 +9739,9 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"big.js": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.0.tgz",
"integrity": "sha512-paIKvJiAaOYdLt6MfnvxkDo64lTOV257XYJyX3oJnJQocIclUn+48k6ZerH/c5FxWE6DGJu1TKDYis7tqHg9kg==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/big.js/-/big.js-6.2.1.tgz",
"integrity": "sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==",
"dev": true
},
"binary-extensions": {
@ -10254,16 +10296,23 @@
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true
},
"csstype": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz",
"integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==",
"dev": true,
"peer": true
},
"daisyui": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.17.0.tgz",
"integrity": "sha512-U8eA8IvwH0JSi72WbRZdmJcYZek3ERMPFPAVFhnYc6dGnobeIHLGBz8J4q012I9YVDUqXFrWz13rpn+SBdn7qQ==",
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.20.0.tgz",
"integrity": "sha512-97xFBWQOXV/JCVRrGI5xranElKMmXIajDxojTQ1qoYwcWlkfFE7hY4D/NyHVIzlQb+B4xbiXhVPJM7EleRAY8g==",
"dev": true,
"requires": {
"color": "^4.2",
"css-selector-tokenizer": "^0.8.0",
"postcss-js": "^4.0.0",
"tailwindcss": "^3.0"
"tailwindcss": "^3"
}
},
"dargs": {
@ -10273,9 +10322,9 @@
"dev": true
},
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz",
"integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==",
"dev": true
},
"dateformat": {
@ -10792,9 +10841,9 @@
"dev": true
},
"froebel": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/froebel/-/froebel-0.17.0.tgz",
"integrity": "sha512-reZdfnUIXBF6a1s6mcwx+DctPRZYGmNcU5kUAq47yrQ4rvpSKw/yN4eCIs4uEFLHUpT7bSBnqd60jC2jN7fbGg==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/froebel/-/froebel-0.18.0.tgz",
"integrity": "sha512-dPkcmnYFA58qLY29MExdUg6/DopBfSeltd66oR9MosezkkXOdjHt0qvNPQbsaAdavJkzqEusiWxOa5TvFjMo5g==",
"dev": true
},
"fs.realpath": {
@ -11013,6 +11062,14 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"goober": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.10.tgz",
"integrity": "sha512-7PpuQMH10jaTWm33sQgBQvz45pHR8N4l3Cu3WMGEWmHShAcTuuP7I+5/DwKo39fwti5A80WAjvqgz6SSlgWmGA==",
"dev": true,
"peer": true,
"requires": {}
},
"graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
@ -12544,9 +12601,9 @@
"dev": true
},
"qrcode": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.0.tgz",
"integrity": "sha512-9MgRpgVc+/+47dFvQeD6U2s0Z92EsKzcHogtum4QB+UNd025WOJSHvn/hjk9xmzj7Stj95CyUAs31mrjxliEsQ==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.1.tgz",
"integrity": "sha512-nS8NJ1Z3md8uTjKtP+SGGhfqmTCs5flU/xR623oI0JX+Wepz9R8UrRVCTBTJm3qGw3rH6jJ6MUHjkDx15cxSSg==",
"dev": true,
"requires": {
"dijkstrajs": "^1.0.1",
@ -12975,10 +13032,17 @@
"integrity": "sha512-RKHyFQ+J5lXyE/SoyJVHgTBeBck2etYVJn1/9F7ehlzyD2pIOMqLpNXD1GfWQljHqNdXZBSyE+xB/Cck5l9Q/g==",
"requires": {}
},
"solid-confetti-explosion": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/solid-confetti-explosion/-/solid-confetti-explosion-1.0.3.tgz",
"integrity": "sha512-rAxyxcyhkb7ZCO9fU6YAbH65YH/4+4sCAyaGasd4WzX7XZEpV4bO+VYGYNDV78e/RDwB/8b+PWH25fqwI/igHw==",
"dev": true,
"requires": {}
},
"solid-js": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.5.tgz",
"integrity": "sha512-32NGpuabEJDTeQ7fjaTR2TLC7R/X5hbqhYdEQ1e+GcIK8r8+/V0Nv17eZQii5Z/97/mtdt8yi63chzg73qnz/A=="
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.8.tgz",
"integrity": "sha512-XErZdnnYYXF7OwGSUAPcua2y5/ELB/c53zFCpWiEGqxTNoH1iQghzI8EsHJXk06sNn+Z/TGhb8bPDNNGSgimag=="
},
"solid-meta": {
"version": "0.27.5",
@ -13041,6 +13105,17 @@
"sirv": "^1.0.12"
}
},
"solid-styled-components": {
"version": "0.28.4",
"resolved": "https://registry.npmjs.org/solid-styled-components/-/solid-styled-components-0.28.4.tgz",
"integrity": "sha512-SGbXv5tLIs1qErr3x7M+HWE4lu+37C4myV8gbce7WnZumjBmM5sifKv/NulVeSf3nMRa3uwkAM14q7QmLGC2gQ==",
"dev": true,
"peer": true,
"requires": {
"csstype": "^3.1.0",
"goober": "^2.1.10"
}
},
"sortablejs": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
@ -13302,9 +13377,9 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
"tailwindcss": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.4.tgz",
"integrity": "sha512-NrxbFV4tYsga/hpWbRyUfIaBrNMXDxx5BsHgBS4v5tlyjf+sDsgBg5m9OxjrXIqAS/uR9kicxLKP+bEHI7BSeQ==",
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.1.6.tgz",
"integrity": "sha512-7skAOY56erZAFQssT1xkpk+kWt2NrO45kORlxFPXUt3CiGsVPhH1smuH5XoDH6sGPXLyBv+zgCKA2HWBsgCytg==",
"dev": true,
"requires": {
"arg": "^5.0.2",
@ -13328,7 +13403,7 @@
"postcss-selector-parser": "^6.0.10",
"postcss-value-parser": "^4.2.0",
"quick-lru": "^5.1.1",
"resolve": "^1.22.0"
"resolve": "^1.22.1"
},
"dependencies": {
"glob-parent": {
@ -13570,9 +13645,9 @@
}
},
"unplugin-icons": {
"version": "0.14.6",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.6.tgz",
"integrity": "sha512-8sxDiL4l+TV4zufZfrskgHZZSDFoGOCBgYsefRMM4inQ3Z6KhgMSuNyew7U7D/xG//rwxgD7bN+Dv+YAZEEfEw==",
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-0.14.7.tgz",
"integrity": "sha512-TrNnEdpaXMdiG5BsCgvU6cv/gSLYvIk1f8wGCGZmOo4wmi3nqYBuqIEuiXhmmyXdDZuRRpCaOzCnCYYZ5H7U8g==",
"requires": {
"@antfu/install-pkg": "^0.1.0",
"@antfu/utils": "^0.5.2",
@ -13644,9 +13719,9 @@
}
},
"vite": {
"version": "2.9.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.13.tgz",
"integrity": "sha512-AsOBAaT0AD7Mhe8DuK+/kE4aWYFMx/i0ZNi98hJclxb4e0OhQcZYUrvLjIaQ8e59Ui7txcvKMiJC1yftqpQoDw==",
"version": "2.9.14",
"resolved": "https://registry.npmjs.org/vite/-/vite-2.9.14.tgz",
"integrity": "sha512-P/UCjSpSMcE54r4mPak55hWAZPlyfS369svib/gpmz8/01L822lMPOJ/RYW6tLCe1RPvMvOsJ17erf55bKp4Hw==",
"requires": {
"esbuild": "^0.14.27",
"fsevents": "~2.3.2",

@ -1,6 +1,6 @@
{
"name": "rappli",
"version": "1.0.0",
"version": "1.3.0",
"bin": "./bin/rappli.js",
"scripts": {
"dev": "solid-start dev",
@ -21,23 +21,23 @@
"type": "module",
"devDependencies": {
"@formkit/auto-animate": "^1.0.0-beta.1",
"@iconify/json": "^2.1.69",
"@tailwindcss/typography": "^0.5.2",
"@types/big.js": "^6.1.4",
"@iconify/json": "^2.1.81",
"@tailwindcss/typography": "^0.5.4",
"@types/big.js": "^6.1.5",
"@types/qrcode": "^1.4.2",
"@types/sortablejs": "^1.13.0",
"autoprefixer": "^10.4.2",
"autosize": "^5.0.1",
"big.js": "^6.2.0",
"daisyui": "^2.17.0",
"date-fns": "^2.28.0",
"froebel": "^0.17.0",
"big.js": "^6.2.1",
"daisyui": "^2.20.0",
"date-fns": "^2.29.1",
"froebel": "^0.18.0",
"myzod": "^1.8.7",
"nanoid": "^4.0.0",
"node-iso11649": "^2.1.2",
"postcss": "^8.4.14",
"prettier": "^2.7.1",
"qrcode": "^1.5.0",
"qrcode": "^1.5.1",
"rehype-stringify": "^9.0.3",
"remark": "^14.0.2",
"remark-breaks": "^3.0.2",
@ -46,13 +46,14 @@
"remark-rehype": "^10.1.0",
"shareon": "^2.0.0",
"solid-app-router": "^0.4.1",
"solid-js": "^1.4.5",
"solid-confetti-explosion": "^1.0.3",
"solid-js": "^1.4.8",
"solid-meta": "^0.27.5",
"sortablejs": "^1.15.0",
"standard-version": "^9.5.0",
"tailwindcss": "^3.1.4",
"tailwindcss": "^3.1.6",
"typer-js": "^5.5.3",
"typescript": "^4.6.2",
"typescript": "^4.7.4",
"unified": "^10.1.2"
},
"engines": {
@ -61,8 +62,8 @@
"dependencies": {
"solid-start": "v0.1.0-alpha.89",
"solid-start-node": "next",
"unplugin-icons": "^0.14.6",
"vite": "^2.9.13"
"unplugin-icons": "^0.14.7",
"vite": "^2.9.14"
},
"standard-version": {
"scripts": {

Binary file not shown.

@ -1,26 +1,31 @@
import { formatISO9075, fromUnixTime, getUnixTime } from "date-fns";
import {
Component,
JSX,
Show,
mergeProps,
splitProps,
FlowComponent,
createSignal,
createEffect,
For,
} from "solid-js";
import { validateInput } from "~/hooks/validation";
import AsteriskIcon from "~icons/ph/asterisk-bold";
import MaximizeIcon from "~icons/carbon/maximize";
import MinimizeIcon from "~icons/carbon/minimize";
import autosize from "autosize";
import type { JSX } from "solid-js";
import {
createNativeInputValue,
createOptionalNumberInputHandler,
} from "~/util";
export const TextInput: Component<
{
label: string;
placeholder?: string;
labelMinWidth?: string;
suffix?: string;
suffix?: string | JSX.Element;
size?: string;
vertical?: boolean;
} & JSX.InputHTMLAttributes<HTMLInputElement>
@ -71,6 +76,7 @@ export const TextInput: Component<
"gap-1": true,
"h-8": props.vertical,
"font-bold": rest.required,
"bg-slate-200/70": vState().valid,
"bg-red-100": !vState().valid,
}}
>
@ -89,13 +95,13 @@ export const TextInput: Component<
[props.class || ""]: true,
}}
type="text"
lang={rest.type === "number" ? "en" : undefined}
placeholder={props.placeholder}
{...rest}
/>
<Show when={props.suffix}>
<span
classList={{
"bg-slate-200/70": vState().valid,
"bg-red-100": !vState().valid,
}}
>
@ -154,7 +160,7 @@ export const TextArea: Component<
</Show>
</button>
<label class="input-group input-group-vertical">
<span class="h-8 flex gap-2 justify-between pr-14">
<span class="h-8 bg-slate-200/70 flex gap-2 justify-between pr-14">
{props.label}
{props.labelSuffixJsx}
</span>
@ -216,3 +222,79 @@ export const UnixDateInput: Component<
/>
);
};
export const NumberInput: Component<
{ value?: number; onInput: (v: number | undefined) => void } & Omit<
Parameters<typeof TextInput>[0],
"onInput"
>
> = (p) => {
let el: HTMLInputElement = undefined!;
const [props, rest] = splitProps(p, ["value", "onInput"]);
const value = createNativeInputValue(
() => el,
() => props.value
);
return (
<TextInput
ref={el}
value={value()}
maxLength={9}
onInput={createOptionalNumberInputHandler(props.onInput)}
{...rest}
/>
);
};
// TODO: Move input-group into separate component
export const Select: Component<
{
value: string | number;
options: [string | number, string][];
label: string;
labelMinWidth?: string;
onChange: (v: any) => void;
} & JSX.InputHTMLAttributes<HTMLSelectElement>
> = (p) => {
p = mergeProps(
{
labelMinWidth: "95px",
},
p
);
const [props, rest] = splitProps(p, [
"value",
"options",
"label",
"labelMinWidth",
"onChange",
]);
return (
<div class="shrink form-control">
<label class="input-group">
<span
class="bg-slate-200/70"
style={{ "min-width": props.labelMinWidth }}
>
{props.label}
</span>
<select
class="flex-1 select select-sm select-bordered"
onChange={(e) => props.onChange(e.currentTarget.value)}
{...rest}
>
<For each={props.options}>
{([type, label]) => (
<option selected={type === props.value} value={type}>
{label}
</option>
)}
</For>
</select>
</label>
</div>
);
};

@ -20,7 +20,7 @@ const Modal: FlowComponent<{ open: boolean }> = (props) => {
"!visible": isVisible(),
}}
>
<div class="modal-box relative py-16 w-11/12 lg:w-2/3 xxl:w-1/2 lg:p-16 max-w-none overflow-hidden">
<div class="modal-box relative py-16 w-11/12 lg:w-3/4 xl:w-2/3 xxl:w-1/2 lg:p-16 max-w-none overflow-hidden">
{props.children}
</div>
</div>

@ -48,7 +48,7 @@ const Page: FlowComponent = (props) => {
});
return (
<div class="flex-1 flex flex-col justify-start print:block overflow-y-hidden overflow-x-hidden xxl:overflow-y-auto items-center xl:max-w-[none] max-h-screen xl:max-h-[none] print:max-h-[none] min-h-screen print:min-h-0 p-5 pt-3 print:p-0">
<div class="flex-1 flex flex-col justify-start print:block overflow-y-hidden overflow-x-hidden xxl:overflow-y-auto items-center lg:max-w-[none] max-h-screen lg:max-h-[none] print:max-h-[none] min-h-screen print:min-h-0 p-5 pt-3 print:p-0">
<div
aria-disabled="true"
class="print:hidden text-xs text-black text-opacity-50 uppercase mb-2"
@ -71,7 +71,7 @@ const Page: FlowComponent = (props) => {
{lastSavedWords()}
</span>
</div>
<div class="w-[1200px] shrink-0 scale-[.5] lg:scale-90 xl:scale-100 origin-top print:scale-100 shadow-md print:shadow-none print:w-full bg-white p-5 print:p-0">
<div class="w-[900px] lg:w-[1000px] xl:w-[1100px] xxl:w-[1200px] print:w-full shrink-0 scale-75 md:scale-90 lg:scale-100 origin-top print:scale-100 shadow-md print:shadow-none bg-white py-5 print:p-0">
{props.children}
</div>
</div>

@ -146,7 +146,7 @@ const Positions: Component<{
</td>
<td
classList={{
"align-top text-center": true,
"align-top text-center !whitespace-nowrap": true,
"border-b-0 pb-0": hasTwoRows(),
}}
>
@ -154,7 +154,7 @@ const Positions: Component<{
</td>
<td
classList={{
"align-top text-right": true,
"align-top text-right !whitespace-nowrap": true,
"border-b-0 pb-0": hasTwoRows(),
}}
>
@ -167,7 +167,7 @@ const Positions: Component<{
</td>
<td
classList={{
"align-top text-right ": true,
"align-top text-right !whitespace-nowrap": true,
"border-b-0 pb-0": hasTwoRows(),
"line-through": position.fixedDiscountPrice != null,
}}
@ -185,7 +185,7 @@ const Positions: Component<{
/>
</Show>
</td>
<td class="align-top pt-1 text-right">
<td class="align-top pt-1 text-right !whitespace-nowrap">
<Show when={position.fixedDiscountPrice != null}>
{formatAmount(position.fixedDiscountPrice!)} CHF
</Show>
@ -202,7 +202,7 @@ const Positions: Component<{
<td class="align-bottom">Summe</td>
<td></td>
<td></td>
<td class="text-right align-bottom">
<td class="text-right align-bottom !whitespace-nowrap">
{formatAmount(props.invoiceData.amountBeforeTax)} CHF
</td>
</tr>
@ -214,7 +214,7 @@ const Positions: Component<{
</td>
<td></td>
<td></td>
<td class="text-right">
<td class="text-right !whitespace-nowrap">
{formatAmount(props.invoiceData.tax)} CHF
</td>
</tr>
@ -224,7 +224,7 @@ const Positions: Component<{
<td class="align-bottom">Gesamtbetrag</td>
<td></td>
<td></td>
<td class="align-bottom text-right">
<td class="align-bottom text-right !whitespace-nowrap">
{formatAmount(props.invoiceData.amount)} CHF
</td>
</tr>

@ -2,10 +2,13 @@ import {
Component,
createEffect,
createResource,
createSignal,
JSX,
Show,
splitProps,
} from "solid-js";
import qrcode from "qrcode";
import WarningIcon from "~icons/carbon/warning-alt-filled";
const QrCode: Component<
{
@ -21,15 +24,26 @@ const QrCode: Component<
]);
let ref: HTMLDivElement = undefined!;
const [qrError, setQrError] = createSignal<string | undefined>();
const [svg] = createResource(
() => props.value,
(value) => {
return qrcode.toString(value, {
type: "svg",
margin: props.margin,
errorCorrectionLevel: props.errorCorrectionLevel as any,
});
async (value) => {
let svg;
try {
svg = await qrcode.toString(value, {
type: "svg",
margin: props.margin,
errorCorrectionLevel: props.errorCorrectionLevel as any,
});
setQrError();
} catch (err) {
console.error(err);
setQrError((err as Error).message);
}
return svg;
}
);
@ -40,7 +54,17 @@ const QrCode: Component<
});
});
return <div ref={ref} {...rest}></div>;
return (
<>
<Show when={qrError()}>
<div class="absolute h-full p-3 gap-2 rounded-sm flex items-start bg-error/80 backdrop-blur-sm ring-2 ring-error ring-offset-2 border border-error text-error-content overflow-y-auto">
<WarningIcon width="2em" height="2em" class="flex-shrink-0" /> Fehler:{" "}
{qrError()}
</div>
</Show>
<div ref={ref} {...rest}></div>
</>
);
};
export default QrCode;

File diff suppressed because it is too large Load Diff

@ -21,9 +21,13 @@ import AddIcon from "~icons/carbon/add-filled";
import DeleteIcon from "~icons/carbon/trash-can";
import DragVerticalIcon from "~icons/carbon/drag-vertical";
import PositionSettingsIcon from "~icons/carbon/settings-adjust";
import { Checkbox, TextArea, TextInput } from "../Form";
import { Checkbox, NumberInput, TextArea, TextInput } from "../Form";
import { MarkdownHelpLabel } from "../Markdown";
import { createOptionalNumberInputHandler } from "~/util";
import {
createOptionalNumberInputHandler,
createNativeInputValue,
resetInput,
} from "~/util";
export const PositionsSettings: Component = () => {
const [state, setState] = useContext(StoreContext)!;
@ -125,6 +129,12 @@ export const PositionsSettings: Component = () => {
);
};
let quantityInputEl: HTMLInputElement = undefined!;
const quantityValue = createNativeInputValue(
() => quantityInputEl,
() => position.quantity
);
return (
<div class="indicator w-full">
<div class="indicator-item indicator-middle indicator-end flex items-center">
@ -243,24 +253,24 @@ export const PositionsSettings: Component = () => {
<Show when={position.type === POSITION_TYPE_QUANTITY}>
<div class="flex-1">
<input
ref={quantityInputEl}
class="w-full input input-bordered input-xs"
value={
position.quantity === 0
? ""
: position.quantity
}
value={quantityValue()}
placeholder="Menge"
min="0"
required
type="number"
onInput={(e) => {
setState(
"positions",
idx(),
"quantity",
parseFloat(e.currentTarget.value) || 0
);
}}
name="Menge"
onInput={createOptionalNumberInputHandler(
(v) => {
v != null &&
setState(
"positions",
idx(),
"quantity",
v
);
}
)}
onBlur={resetInput(0)}
/>
</div>
</Show>
@ -293,7 +303,7 @@ export const PositionsSettings: Component = () => {
</Show>
</div>
</div>
<TextInput
<NumberInput
size="xs"
value={position.itemPrice}
label="Einzelpreis"
@ -302,12 +312,9 @@ export const PositionsSettings: Component = () => {
? state.defaultItemPrice + ""
: undefined
}
step="0.01"
min="0"
type="number"
onInput={createOptionalNumberInputHandler((v) =>
onInput={(v) =>
setState("positions", idx(), "itemPrice", v)
)}
}
/>
<div
use:autoAnimate
@ -348,21 +355,18 @@ export const PositionsSettings: Component = () => {
/>
</div>
<div class="col-span-2">
<TextInput
<NumberInput
label="Aktionspreis"
suffix="CHF"
type="number"
step="0.01"
min="0"
value={position.fixedDiscountPrice}
onInput={createOptionalNumberInputHandler((v) =>
onInput={(v) =>
setState(
"positions",
idx(),
"fixedDiscountPrice",
v
)
)}
}
/>
</div>
<div class="col-span-2">

@ -8,13 +8,22 @@ import {
JSX,
useContext,
splitProps,
createSignal,
Show,
For,
untrack,
createResource,
} from "solid-js";
import { createStore } from "solid-js/store";
import LufraiLogo from "~icons/custom/lufrai-logo";
import AppIcon from "~icons/custom/icon";
import ExternalLinkIcon from "~icons/carbon/launch";
import WarningIcon from "~icons/carbon/warning-alt-filled";
import LaunchIcon from "~icons/carbon/edit";
import DonationIcon from "~icons/carbon/favorite-filled";
import DonationIcon from "~icons/bxs/donate-heart";
import FavoriteIcon from "~icons/carbon/favorite";
import FavoriteHalfIcon from "~icons/carbon/favorite-half";
import FavoriteFilledIcon from "~icons/carbon/favorite-filled";
import WatermarkIcon from "~icons/carbon/bullhorn";
import FreedomIcon from "~icons/noto/butterfly";
import FreeIcon from "~icons/noto/seedling";
@ -22,6 +31,8 @@ import PrivacyIcon from "~icons/noto/princess";
import AgileIcon from "~icons/noto/person-bouncing-ball";
import ResultIcon from "~icons/noto/chequered-flag";
import HugIcon from "~icons/noto/hugging-face";
import VideoIcon from "~icons/carbon/video-filled";
import CloseIcon from "~icons/carbon/close-outline";
import Modal, { ModalCloseButton } from "./Modal";
import { LocalStoreContext } from "~/stores";
import createAccordion from "./Accordion";
@ -33,16 +44,19 @@ import {
getDomain,
getHost,
onClickFocus,
shuffle,
} from "~/util";
import { capitalize } from "froebel";
import { capitalize, clamp, shuffle } from "froebel";
import { markdownHelpUrl } from "./Markdown";
import AgileCalculator from "./AgileCalculator";
import server from "solid-start/server";
import { getLikes, increaseLikes } from "~/server/likes";
export const description =
"Räppli ist eine freie Web App zur Erstellung von Schweizerischen Rechnungen inklusive QR-Code. Erfasse deine Rechnungspositionen und erhalte unmittelbar eine druckbare Rechnung.";
const externalLinkClass = "inline-flex items-center gap-1";
import ConfettiExplosion from "solid-confetti-explosion";
// TODO: after solid-start update >= alpha.95, switch to lazy
// const ConfettiExplosion = lazy(() => import("solid-confetti-explosion"));
const ExternalLink: FlowComponent<
JSX.AnchorHTMLAttributes<HTMLAnchorElement>
@ -64,6 +78,9 @@ const WelcomeModal: Component = (props) => {
const isOpen = createMemo(() => {
return localStateMounted() && localState.showWelcome;
});
const [showTrailer, setShowTrailer] = createSignal(false);
const increaseCount_ = server(increaseLikes);
const [likes] = createResource(server(getLikes));
onMount(function () {
let adjectives = [
@ -124,7 +141,7 @@ const WelcomeModal: Component = (props) => {
// typer-js has some weird logic how it handles halt / resume. Essentially if halt wasn't executed before of resume, resume breaks completely
halted && typerInstance.resume();
} catch (err) {
if ((err as TypeError).message != "u.resume is not a function") {
if (!(err as TypeError).message.endsWith("is not a function")) {
console.dir(err);
}
}
@ -164,10 +181,33 @@ const WelcomeModal: Component = (props) => {
"Rahel Lutz",
]);
thankYouRahelAndFredi.splice(1, 0, " und ");
const [likeCount, setLikeCount] = createSignal(0);
const [confetti, setConfetti] = createStore([false, false, false]);
const confettiDuration = 2000;
createEffect(function () {
if (likeCount() == 0) {
return;
}
untrack(function () {
for (const idx of confetti.keys()) {
if (confetti[idx]) {
continue;
}
setConfetti(idx, true);
setTimeout(
() => setConfetti(idx, false),
(confettiDuration / 100) * 80
);
break;
}
});
});
return (
<Modal open={isOpen()}>
<div class="hidden xl:block">
<div class="hidden lg:block">
<ModalCloseButton onClick={() => setLocalState("showWelcome", false)} />
</div>
<div class="max-h-[60vh] overflow-y-auto px-2">
@ -199,7 +239,37 @@ const WelcomeModal: Component = (props) => {
</div>
</div>
<div class="mt-28 text-base flex flex-wrap text-center gap-7 items-center justify-center">
<Show when={showTrailer()}>
{() => (
<div class="absolute top-0 left-0 h-full w-full flex flex-col justify-center items-center gap-8 bg-black backdrop-blur bg-opacity-90 [@supports(backdrop-filter:blur(0))]:bg-opacity-70 z-10">
<video
controls
autoplay
onended={() => setShowTrailer(false)}
>
<source src="/RaeppliTrailer.mp4" type="video/mp4" />
</video>
<button
class="btn btn-accent btn-xl gap-2"
onClick={() => setShowTrailer(false)}
>
<CloseIcon />
Schliessen
</button>
</div>
)}
</Show>
<div class="mt-24 text-base grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 text-center gap-y-3 md:gap-y-5 gap-x-2 lg:gap-x-8 items-center justify-center">
<button
class="shadow-md btn btn-secondary hover:ring-2 ring-offset-2 ring-secondary bg-pink-600 hover gap-2 whitespace-normal flex-nowrap"
onClick={() => setShowTrailer(true)}
>
<VideoIcon />
<span>
Trailer <span class="hidden xl:inline">anschauen</span>
</span>
</button>
<a
class="link text-secondary-focus hover:text-secondary"
href="#welcome-quickstart"
@ -210,8 +280,7 @@ const WelcomeModal: Component = (props) => {
class="link text-secondary-focus hover:text-secondary"
href="#welcome-why-free"
>
Wieso ist Räppli <br />
<strong>komplett kostenlos</strong>?
<strong>Komplett kostenlos</strong>!
</a>
<a
class="link text-secondary-focus hover:text-secondary"
@ -223,18 +292,16 @@ const WelcomeModal: Component = (props) => {
class="link text-secondary-focus hover:text-secondary"
href="#welcome-patron"
>
Das Projekt
<br />
unterstützen
Das Projekt unterstützen
</a>
<a
class="link text-secondary-focus hover:text-secondary flex items-center gap-2"
class="link text-secondary-focus hover:text-secondary"
href="#welcome-opensource"
>
Open Source
</a>
<a
class="link text-secondary-focus hover:text-secondary flex items-center gap-2"
class="link text-secondary-focus hover:text-secondary"
href="#welcome-thankyou"
>
Danksagungen
@ -922,16 +989,16 @@ const WelcomeModal: Component = (props) => {
</section>
</div>
</div>
<div class="mt-14 relative flex justify-center items-center gap-10">
<div class="mt-8 xl:mt-14 relative flex justify-center items-center gap-2 sm:gap-5 xl:gap-10">
<a
href="#welcome-patron"
class="hidden xl:flex btn btn-sm btn-secondary bg-purple-600 border-purple-600 gap-2 transition-all shadow-lg hover:shadow-lg shadow-purple-300 hover:shadow-purple-500 hover:ring-2 ring-offset-2 ring-purple-500"
class="flex btn btn-sm btn-secondary bg-purple-600 border-purple-600 gap-2 transition-all shadow-lg hover:shadow-lg shadow-purple-300 hover:shadow-purple-500 hover:ring-2 ring-offset-2 ring-purple-500"
>
<DonationIcon />
Spenden
</a>
<div
class="hidden xl:flex form-control flex-row items-center gap-2 tooltip tooltip-accent"
class="hidden lg:flex form-control flex-row items-center gap-2 tooltip tooltip-accent"
data-tip="Füge das Lufrai Logo zu deinem Dokument hinzu."
>
<input
@ -951,16 +1018,66 @@ const WelcomeModal: Component = (props) => {
Für Lufrai werben
</label>
</div>
<div
class="static md:relative lg:hidden tooltip tooltip-error"
data-tip="Bitte verwende einen Laptop oder Computer mit einer
Mindestbreite von 1024 Pixeln!"
>
<div class="btn hover:!bg-white hover:!text-black/50 btn-outline flex gap-2 transition-all shadow-indigo-300 hover:shadow-red-300 hover:ring-2 ring-offset-2 ring-red-400">
<WarningIcon /> Loslegen
</div>
</div>
<button
class="btn relative border-red-600 bg-red-500 hover:bg-red-600 hover:border-pink-700 hover:ring-2 ring-offset-2 ring-pink-500 shadow-red-300 shadow-lg lg:shadow-md lg:shadow-red-300 hover:shadow-red-400 tooltip tooltip-error stroke-1 stroke-white"
onClick={() => {
setLikeCount(likeCount() + 1);
increaseCount_();
}}
data-tip={(likes() || 0) + likeCount()}
>
<div
class="flex justify-center transition-transform"
style={{
transform: `scale(${clamp(0.9, 0.9 + likeCount() / 12, 1.3)})`,
}}
>
<Show when={likeCount() === 0}>
<FavoriteIcon />
</Show>
<Show when={likeCount() === 1}>
<FavoriteHalfIcon />
</Show>
<Show when={likeCount() > 1}>
<FavoriteFilledIcon />
</Show>
</div>
<span aria-disabled="true" class="text-xs text-orange-200 font-black">
{Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
}).format((likes() || 0) + likeCount())}
</span>
<For each={confetti}>
{(v) => (
<Show when={v}>
<ConfettiExplosion
stageHeight={1500}
duration={confettiDuration}
force={1}
shouldDestroyAfterDone={true}
/>
</Show>
)}
</For>
</button>
<button
onClick={() => setLocalState("showWelcome", false)}
class="hidden xl:flex btn btn-xl btn-primary gap-2 transition-all shadow-lg hover:shadow-lg shadow-indigo-300 hover:shadow-indigo-500 hover:ring-2 ring-offset-2 ring-indigo-500"
class="hidden lg:flex btn btn-lg btn-primary gap-2 transition-all shadow-lg shadow-indigo-300 hover:shadow-indigo-500 hover:ring-2 ring-offset-2 ring-indigo-500"
>
<LaunchIcon />
Loslegen
</button>
<div class="w-full text-sm xl:hidden bg-error bg-opacity-50 ring-2 ring-offset-2 ring-error text-error-content rounded p-3 shadow-2xl flex items-center justify-center gap-3">
<WarningIcon /> Bitte verwende einen Laptop oder Computer!
</div>
</div>
<a
href="https://git.lufrai.com/rappli/rappli/src/branch/master/CHANGELOG.md#changelog"

@ -3,6 +3,9 @@ import {
createHandler,
renderAsync,
} from "solid-start/entry-server";
import { state } from "./server/store";
state();
export default createHandler(
renderAsync((context) => <StartServer context={context} />)

@ -7,11 +7,15 @@ export const createLocalStore = function <T extends Record<string, any>>(
prefix = "app",
serializer = (v: any) => JSON.stringify(v),
deserializer = (v: any) => JSON.parse(v),
migrate = undefined as
| ((getState: () => Record<string, any>) => boolean)
| undefined,
} = {}
) {
const [state, setState] = createStore(initState);
const [mounted, setMounted] = createSignal(false);
const localStorage = globalThis.localStorage;
const storePrefix = `${prefix}-`;
if (localStorage) {
let mounts = 0;
@ -20,8 +24,36 @@ export const createLocalStore = function <T extends Record<string, any>>(
const keys = Object.keys(state);
const changedBeforeMount = {} as Record<string, any>;
if (migrate) {
let migratedState: Record<string, any> | undefined = undefined;
const getMigrateState = () => {
if (migratedState) return migratedState;
migratedState = {};
for (const key of Object.keys(localStorage)) {
if (!key.startsWith(storePrefix)) {
continue;
}
migratedState[key.slice(storePrefix.length)] = deserializer(
localStorage.getItem(key)
);
}
return migratedState;
};
const migrated = migrate(getMigrateState);
if (migrated && migratedState != undefined) {
for (const [key, value] of Object.entries(migratedState)) {
localStorage.setItem(`${storePrefix}${key}`, serializer(value));
}
}
migratedState = undefined;
}
for (const key of keys) {
let storeKey = `${prefix}-${key}`;
let storeKey = `${storePrefix}${key}`;
let mountValue = localStorage.getItem(storeKey);
let initRun = true;
const [updatingCount, setUpdatingCount] = createSignal(0);

@ -22,6 +22,8 @@ import Positions, {
} from "~/components/Positions";
import SettingsOverlay from "~/components/Settings/Overlay";
import {
ADDRESS_LAYOUT_LEFT,
ADDRESS_LAYOUT_RIGHT,
createLocalStore,
createStore,
createUiStore,
@ -92,7 +94,15 @@ export default function Home() {
});
const InnerPadding: FlowComponent = (props) => (
<div classList={{ "px-16": state.fullWidthInvoice }}>{props.children}</div>
<div
classList={{
"mx-16": true,
"print:mx-0": !state.fullWidthInvoice,
"print:mx-[11mm]": state.fullWidthInvoice,
}}
>
{props.children}
</div>
);
const PageHeader: Component = () => {
@ -149,8 +159,15 @@ export default function Home() {
<Meta property="og:url" content={getHost()} />
<Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:site" content="@katy_wings" />
<Show when={localState.logo}>
<div class="flex justify-end items-center h-20 mb-5">
<div
classList={{
"flex justify-start items-center print:mb-7 print:h-28": true,
"h-20 mb-7": !!localState.logo,
"justify-start": localState.addressLayout == ADDRESS_LAYOUT_RIGHT,
"justify-end": localState.addressLayout == ADDRESS_LAYOUT_LEFT,
}}
>
<Show when={localState.logo}>
<img
classList={{
"max-h-full max-w-[50%] w-auto": true,
@ -161,75 +178,88 @@ export default function Home() {
height={localState.logo?.height}
src={localState.logo?.url}
/>
</div>
</Show>
<div class="text-xs mb-2">
{address().name
? [address().name, getLine1(address()), getLine2(address())]
.filter((x) => x != "")
.join(" · ")
: ""}
</Show>
</div>
<div class="grid grid-cols-2 gap-x-[30%] mb-10">
<div class="leading-snug">
<div>{customerAddress().name}</div>
<div>{getLine1(customerAddress())}</div>
<div>{getLine2(customerAddress())}</div>
</div>
<div class="grid grid-cols-2 gap-x-4 text-sm">
<RightItem
class="font-bold"
label="Projekt Nr."
value={state.project.projectNumber}
/>
<RightItem
class="font-bold"
label="Bestellungs Nr."
value={state.project.orderNumber}
/>
<RightItem
value={getDisplayDateFromUnix(state.project.date)}
label="Datum"
/>
<RightItem
label="Lieferungs Nr."
value={state.project.deliveryNumber}
/>
<RightItem
label="Lieferdatum"
value={
state.project.deliveryDate != null &&
getDisplayDateFromUnix(state.project.deliveryDate)
}
/>
<RightItem
label="Kunden Nr."
value={state.customer.customerNumber}
/>
<RightItem label="Ihre MwST-Nr." value={state.customer.vatNumber} />
<Show
when={
localState.contact.name ||
localState.contact.phone ||
localState.contact.email
}
<div class="print:min-h-[12rem] mb-10">
<div class="grid grid-cols-2 gap-x-[15%]">
<div
classList={{
"order-last": localState.addressLayout == ADDRESS_LAYOUT_RIGHT,
}}
>
<hr class="col-span-2 my-2" />
<RightItem
value={localState.contact.name}
label="Ansprechpartner"
/>
<RightItem value={localState.contact.phone} label="Telefon" />
<RightItem
value={localState.contact.email}
label="E-Mail Adresse"
/>
</Show>
<div class="text-sm mb-3">
{address().name
? [address().name, getLine1(address()), getLine2(address())]
.filter((x) => x != "")
.join(" · ")
: ""}
</div>
<div class="text-lg leading-snug">
<div>{customerAddress().name}</div>
<div>{getLine1(customerAddress())}</div>
<div>{getLine2(customerAddress())}</div>
</div>
</div>
<div>
<div class="grid grid-cols-2 gap-x-4 text-sm">
<RightItem
class="font-bold"
label="Projekt Nr."
value={state.project.projectNumber}
/>
<RightItem
class="font-bold"
label="Bestellungs Nr."
value={state.project.orderNumber}
/>
<RightItem
value={getDisplayDateFromUnix(state.project.date)}
label="Datum"
/>
<RightItem
label="Lieferungs Nr."
value={state.project.deliveryNumber}
/>
<RightItem
label="Lieferdatum"
value={
state.project.deliveryDate != null &&
getDisplayDateFromUnix(state.project.deliveryDate)
}
/>
<RightItem
label="Kunden Nr."
value={state.customer.customerNumber}
/>
<RightItem
label="Ihre MwST-Nr."
value={state.customer.vatNumber}
/>
<Show
when={
localState.contact.name ||
localState.contact.phone ||
localState.contact.email
}
>
<hr class="col-span-2 my-2" />
<RightItem
value={localState.contact.name}
label="Ansprechpartner"
/>
<RightItem value={localState.contact.phone} label="Telefon" />
<RightItem
value={localState.contact.email}
label="E-Mail Adresse"
/>
</Show>
<Show when={localState.vatNumber}>
<div class="col-span-2 h-4"></div>
<RightItem label="MwST-Nr." value={localState.vatNumber} />
</Show>
<Show when={localState.vatNumber}>
<div class="col-span-2 h-4"></div>
<RightItem label="MwST-Nr." value={localState.vatNumber} />
</Show>
</div>
</div>
</div>
</div>
</div>
@ -278,9 +308,9 @@ export default function Home() {
<Style type="text/css">{`
@page {
size: A4 portrait;
margin: 11mm ${state.fullWidthInvoice ? 0 : 4}rem 11mm ${
state.fullWidthInvoice ? 0 : 4
}rem;
margin: 25mm ${state.fullWidthInvoice ? 0 : 11}mm 11mm ${
state.fullWidthInvoice ? 0 : 11
}mm;
}
.swissinvoice {
@ -289,7 +319,7 @@ export default function Home() {
@media print {
html {
font-size: 10px;
font-size: 11px;
}
}
`}</Style>
@ -332,7 +362,14 @@ export default function Home() {
<Conclusion />
<LufraiWatermark />
</InnerPadding>
<SwissInvoice value={invoiceData()} />
<div
classList={{
"mx-16 print:mx-0": true,
"mx-0": state.fullWidthInvoice,
}}
>
<SwissInvoice value={invoiceData()} />
</div>
</Page>
</Show>
</>

@ -0,0 +1,8 @@
import { state } from "./store";
export const increaseLikes = () => {
state().likes++;
};
export const getLikes = () => {
return state().likes;
};

@ -0,0 +1,64 @@
import { writeFileSync } from "node:fs";
import { writeFile, readFile } from "node:fs/promises";
import path from "node:path";
import { onExit } from "./util";
const store = <T extends Object>(
name: string,
init: () => T,
{ persistInterval = 60 * 1000 } = {}
) => {
let state = init();
(state as any)._storeName = name;
const filePath = path.join(process.cwd(), `${name}.json`);
const persist = (sync = false) => {
const write = sync ? writeFileSync : writeFile;
return write(filePath, JSON.stringify(state));
};
const load = async () => {
try {
const newState = await readFile(filePath, { encoding: "utf8" });
state = JSON.parse(newState);
if ((state as any)._storeName !== name) {
console.error(`File at "${filePath}" expected to be a Rappli store`);
process.exit(1);
}
} catch (err) {
if (typeof err === "object" && (err as any).code !== "ENOENT") {
console.dir(err);
process.exit(1);
}
}
};
const globals = globalThis as any;
(async () => {
await load();
const intervalKey = `_store-persist-${name}`;
if (globals[intervalKey] != null) {
clearInterval(globals[intervalKey]);
}
globals[intervalKey] = setInterval(function () {
persist();
}, persistInterval);
})();
const exitKey = `_store-exit-${name}`;
if (globals[exitKey]) {
globals[exitKey]();
}
globals[exitKey] = onExit(() => persist(true));
const getState = () => state;
return [getState] as [typeof getState];
};
export const [state] = store("data", () => ({
version: 0,
likes: 0,
}));

@ -0,0 +1,38 @@
export const onExit = (cb: () => void) => {
/*
const origCb = cb;
cb = () => {
origCb();
console.log("finished");
};
*/
const cbAndExit = () => {
cb();
process.exit();
};
const exitSignals = [
"SIGINT",
"SIGTERM",
"uncaughtException",
"SIGUSR1",
"SIGUSR2",
];
const cbSignals = ["beforeExit"];
for (const signal of exitSignals) {
process.on(signal, cbAndExit);
}
for (const signal of cbSignals) {
process.on(signal, cb);
}
return () => {
for (const signal of exitSignals) {
process.off(signal, cbAndExit);
}
for (const signal of cbSignals) {
process.off(signal, cb);
}
};
};

@ -49,6 +49,12 @@ export const createUiStore = () =>
export type UiStore = ReturnType<typeof createUiStore>;
export const UiStoreContext = createContext<UiStore>();
export const ADDRESS_LAYOUT_LEFT = "LEFT";
export const ADDRESS_LAYOUT_RIGHT = "RIGHT";
export type AddressLayout =
| typeof ADDRESS_LAYOUT_LEFT
| typeof ADDRESS_LAYOUT_RIGHT;
export const storeSchema = z.object({
version: z.number(),
project: z.object({
@ -80,9 +86,15 @@ export const storeSchema = z.object({
});
export type StoreObject = Infer<typeof storeSchema>;
export const CURRENT_VERSION = 1.3;
export const migrateState = (state: Record<string, any>) => {
state.version = CURRENT_VERSION;
};
export const createStore = () =>
createStore_<StoreObject>({
version: 1,
version: CURRENT_VERSION,
project: {
orderNumber: "",
projectNumber: "",
@ -120,6 +132,7 @@ export const localStoreSchema = z.object({
paymentTerms: z.string().optional(),
iban: z.string(),
creditor: addressSchema,
addressLayout: z.literals(ADDRESS_LAYOUT_LEFT, ADDRESS_LAYOUT_RIGHT),
customAddress: addressSchema,
contact: z.object({
name: z.string(),
@ -140,15 +153,31 @@ export const localStoreSchema = z.object({
});
export type LocalStoreObject = Infer<typeof localStoreSchema>;
export const migrateLocalState = (state: Record<string, any>) => {
if (state.version === 1) {
state.addressLayout = ADDRESS_LAYOUT_LEFT;
state.version = 1.3;
}
state.version = CURRENT_VERSION;
};
export const migrateInfoLog = (oldVersion: number, newVersion: number) =>
console.info(
`Migrated document schema from version ${oldVersion} to ${newVersion}`
);
export const createLocalStore = () =>
createLocalStore_<LocalStoreObject>(
{
version: 1,
version: CURRENT_VERSION,
showWelcome: true,
vatNumber: "",
vatRate: 0.0,
paymentTerms: undefined,
creditor: createAddress(),
addressLayout: ADDRESS_LAYOUT_RIGHT,
customAddress: createAddress(),
contact: {
name: "",
@ -160,7 +189,24 @@ export const createLocalStore = () =>
useCustomAddress: false,
iban: "",
},
{ prefix: "invoice-app" }
{
prefix: "invoice-app",
migrate: (getState) => {
const version = JSON.parse(
localStorage.getItem("invoice-app-version") || "-1"
);
if (version == -1) return false;
if (version !== CURRENT_VERSION) {
migrateLocalState(getState());
migrateInfoLog(version, CURRENT_VERSION);
return true;
}
return false;
},
}
);
export type LocalStore = ReturnType<typeof createLocalStore>;

@ -1,6 +1,6 @@
import Big from "big.js";
import { fromUnixTime, intlFormat } from "date-fns";
import { JSX } from "solid-js";
import { createMemo, JSX } from "solid-js";
export const sleep = (timeout: number) =>
new Promise((res) => setTimeout(res, timeout));
@ -46,18 +46,6 @@ export const resetInput =
el.dispatchEvent(event);
};
// https://dev.to/codebubb/how-to-shuffle-an-array-in-javascript-2ikj
export const shuffle = (list: any[]) => {
for (let i = list.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = list[i];
list[i] = list[j];
list[j] = temp;
}
return list;
};
export const getDomain = () =>
import.meta.env.SSR ? process.env.DOMAIN || "localhost" : location.hostname;
@ -90,9 +78,51 @@ export const createOptionalNumberInputHandler = (
return;
}
const value =
e.currentTarget.value == "" ? undefined : e.currentTarget.valueAsNumber;
let value =
e.currentTarget.value == ""
? undefined
: parseNumberInput(e.currentTarget.value);
if (Number.isNaN(value)) {
return;
}
onInput(value);
};
};
const parseNumberInput = (v: string): number => parseFloat(v.replace(",", "."));
export const createNativeInputValue = (
getEl: () => HTMLInputElement,
signal: () => any
) =>
createMemo(function (prev) {
const value = signal();
const el = getEl();
if (!el) {
return value != null ? value : "";
}
const elValue = parseNumberInput(el.value);
// If the element value and signal value are equal, we can skip triggering the memo change by reusing the prev value
let result = elValue == value ? prev : value;
// NaN is always != NaN in js, we have to replace it with a value which has a proper identity
if (Number.isNaN(result)) {
result = undefined;
}
if (result == null) {
result = "";
}
// If both the value and prev value are the same, but the element value is different, we have to update it manually
if (value === prev && elValue != value) {
el.value = result;
}
return result;
});

@ -23,6 +23,9 @@ export default defineConfig({
server: {
host: "0.0.0.0",
},
ssr: {
noExternal: ["solid-confetti-explosion"],
},
define: {
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
__BUILD_TIME__: JSON.stringify(new Date().getTime()),

Loading…
Cancel
Save