Compare commits

...

26 Commits

Author SHA1 Message Date
Katja Lutz aedcc6d000 chore(release): 0.4.0 2 years ago
Katja Lutz 27b819a39e feat: add social crawler image 2 years ago
Katja Lutz 33a856d97e feat: add CHF suffix to discount price input 2 years ago
Katja Lutz eef1631f6c feat: add donation link to WelcomeModal 2 years ago
Katja Lutz 05c563cbc3 feat: add quantity row to AgileCalculator and improve the layout 2 years ago
Katja Lutz 309a9754b0 fix: avoid division by zero in AgileCalculator 2 years ago
Katja Lutz e43be215b5 feat: write patron section in WelcomeModal 2 years ago
Katja Lutz 3680d8699e feat: add Kohei Asai twitter link in WelcomeModal 2 years ago
Katja Lutz 9d314e5ed4 feat: use git src link for MIT license in WelcomeModal 2 years ago
Katja Lutz 1a9f75f97a feat: add thankgiving for Kohei Asai in WelcomeModal 2 years ago
Katja Lutz 5de43a9a02 feat: write the why-free section in WelcomeModal 2 years ago
Katja Lutz 14f7d2bae3 feat: move Katja mention in WelcomeModal thanksgivings to the top 2 years ago
Katja Lutz 9ae72e2122 feat: implement agile help section in WelcomeModal faq 2 years ago
Katja Lutz 9c5a8fd2c4 feat: implemented AgileCalculator component 2 years ago
Katja Lutz 0a599cf9d4 feat: use decimal period instead of comma character in number inputs 2 years ago
Katja Lutz 32635cf69e feat: rework number input handlers and add proper step props 2 years ago
Katja Lutz a9d34cb17e refactor: simplify agile quantity calculation and export it 2 years ago
Katja Lutz bc3c67f39c feat: cleanup external links in WelcomeModal 2 years ago
Katja Lutz 9c56487918 feat: improve info about further development in WelcomeModal 2 years ago
Katja Lutz 6e023cd17e feat: add micro guide about markdown in WelcomeModal 2 years ago
Katja Lutz 37020b0722 feat: add info about further development to WelcomeModal 2 years ago
Katja Lutz e056551e10 feat: add link to changelog on version text in WelcomeModal 2 years ago
Katja Lutz 928ee688e8 feat: improve installation guide formatting in WelcomeModal 2 years ago
Katja Lutz 733296d03c refactor: rename AcordionItem to AccordionItem in WelcomeModal 2 years ago
Katja Lutz bd52b8b0f1 feat: add privacy info to WelcomeModal faq 2 years ago
Katja Lutz 2ee0672ea5 feat: add installation guide to WelcomeModal and README 2 years ago

@ -2,6 +2,39 @@
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. 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.
## [0.4.0](https://git.lufrai.com/rappli/rappli/compare/v0.3.0...v0.4.0) (2022-06-30)
### Features
* add CHF suffix to discount price input ([33a856d](https://git.lufrai.com/rappli/rappli/commit/33a856d97ef789286a6d429930e2578441970f5d))
* add donation link to WelcomeModal ([eef1631](https://git.lufrai.com/rappli/rappli/commit/eef1631f6c5052380feba72f880e9e0330d9626b))
* add info about further development to WelcomeModal ([37020b0](https://git.lufrai.com/rappli/rappli/commit/37020b0722f93c5a10fffb8e58a06bfaf0e1d366))
* add installation guide to WelcomeModal and README ([2ee0672](https://git.lufrai.com/rappli/rappli/commit/2ee0672ea569119f96410a5a81d5854a8cb9a0d4))
* add Kohei Asai twitter link in WelcomeModal ([3680d86](https://git.lufrai.com/rappli/rappli/commit/3680d8699e8ed213c23666016551c71788bd55ca))
* add link to changelog on version text in WelcomeModal ([e056551](https://git.lufrai.com/rappli/rappli/commit/e056551e109385f73e9cfb5c7a604e30550fe410))
* add micro guide about markdown in WelcomeModal ([6e023cd](https://git.lufrai.com/rappli/rappli/commit/6e023cd17e68a1d649ce0bb65d47e2bed10314e8))
* add privacy info to WelcomeModal faq ([bd52b8b](https://git.lufrai.com/rappli/rappli/commit/bd52b8b0f1e898318f6703c49c66f25e417c5aac))
* add quantity row to AgileCalculator and improve the layout ([05c563c](https://git.lufrai.com/rappli/rappli/commit/05c563cbc399e5c412d6197fb1fa3668d9330e70))
* add social crawler image ([27b819a](https://git.lufrai.com/rappli/rappli/commit/27b819a39e423177cbddd87255cf6dc70be9fecd))
* add thankgiving for Kohei Asai in WelcomeModal ([1a9f75f](https://git.lufrai.com/rappli/rappli/commit/1a9f75f97a11231d2815070406382e121ec4dfbd))
* cleanup external links in WelcomeModal ([bc3c67f](https://git.lufrai.com/rappli/rappli/commit/bc3c67f39c61b1c4442eada32bf942590b43e8a3))
* implement agile help section in WelcomeModal faq ([9ae72e2](https://git.lufrai.com/rappli/rappli/commit/9ae72e2122750e667bfb7c47ec866577c7a8824c))
* implemented AgileCalculator component ([9c5a8fd](https://git.lufrai.com/rappli/rappli/commit/9c5a8fd2c4d988a9a46463da8e1aa2621e3ca496))
* improve info about further development in WelcomeModal ([9c56487](https://git.lufrai.com/rappli/rappli/commit/9c56487918dbe50d6e5f76fb1f1889750d559cd1))
* improve installation guide formatting in WelcomeModal ([928ee68](https://git.lufrai.com/rappli/rappli/commit/928ee688e860f50b81c782ac5ac459cd0d7849f8))
* move Katja mention in WelcomeModal thanksgivings to the top ([14f7d2b](https://git.lufrai.com/rappli/rappli/commit/14f7d2bae321b838dff76a2495df4ebc82877a96))
* rework number input handlers and add proper step props ([32635cf](https://git.lufrai.com/rappli/rappli/commit/32635cf69eb215e6470c94df338cefae9210cb87))
* use decimal period instead of comma character in number inputs ([0a599cf](https://git.lufrai.com/rappli/rappli/commit/0a599cf9d437c0dfa94bdef9b25cfe1851176f30))
* use git src link for MIT license in WelcomeModal ([9d314e5](https://git.lufrai.com/rappli/rappli/commit/9d314e5ed4cf1a6ac80517fd6b9fc1514ccc8659))
* write patron section in WelcomeModal ([e43be21](https://git.lufrai.com/rappli/rappli/commit/e43be215b5baff3c13ce99700ad64f65b9782978))
* write the why-free section in WelcomeModal ([5de43a9](https://git.lufrai.com/rappli/rappli/commit/5de43a9a0248d6686a2325b1f48a49aa8e918322))
### Bug Fixes
* avoid division by zero in AgileCalculator ([309a975](https://git.lufrai.com/rappli/rappli/commit/309a9754b0831292b4e19ea3d639aad0745888a2))
## [0.3.0](https://git.lufrai.com/rappli/rappli/compare/v0.2.0...v0.3.0) (2022-06-28) ## [0.3.0](https://git.lufrai.com/rappli/rappli/compare/v0.2.0...v0.3.0) (2022-06-28)

@ -2,6 +2,18 @@
[<img height="100" src="./assets/icon-large.png"/>](https://rappli.ch) [<img height="100" src="./assets/icon-large.png"/>](https://rappli.ch)
## Installation
```bash
npm install -g rappli
```
## Running it
```bash
DOMAIN=yourdomain.com rappli
```
## Developing ## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "rappli", "name": "rappli",
"version": "0.3.0", "version": "0.4.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "rappli", "name": "rappli",
"version": "0.3.0", "version": "0.4.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"solid-start": "v0.1.0-alpha.89", "solid-start": "v0.1.0-alpha.89",

@ -1,6 +1,6 @@
{ {
"name": "rappli", "name": "rappli",
"version": "0.3.0", "version": "0.4.0",
"bin": "./bin/rappli.js", "bin": "./bin/rappli.js",
"scripts": { "scripts": {
"dev": "solid-start dev", "dev": "solid-start dev",

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

@ -0,0 +1,156 @@
import Big from "big.js";
import { Component, createMemo } from "solid-js";
import { createStore } from "solid-js/store";
import { TextInput } from "./Form";
import { calculateAgileQuantity } from "./Positions";
import { formatAmount } from "./SwissInvoice";
const AgileCalculator: Component = () => {
const [agileCalculator, setAgileCalculator] = createStore({
minPoints: 0,
maxPoints: 10,
risk: 70,
singlePrice: 10.0,
hoursPerPoint: 1,
});
const hours = createMemo(() =>
calculateAgileQuantity(
agileCalculator.hoursPerPoint,
new Big(agileCalculator.risk).div(100).toNumber(),
agileCalculator.minPoints,
agileCalculator.maxPoints
)
);
const quantity = createMemo(() =>
agileCalculator.hoursPerPoint > 0
? new Big(hours()).div(agileCalculator.hoursPerPoint).toNumber()
: hours()
);
return (
<div class="border border-slate-500/40 flex flex-col gap-2 p-2 rounded mb-3">
<h3 class="mt-0">Rechnungsbeispiel</h3>
<TextInput
labelMinWidth="300px"
label="Risiko Faktor"
suffix="%"
step="10"
min="0"
max="100"
type="number"
value={agileCalculator.risk}
onInput={(e) =>
!Number.isNaN(e.currentTarget.valueAsNumber) &&
setAgileCalculator("risk", e.currentTarget.valueAsNumber)
}
/>
<TextInput
labelMinWidth="300px"
label="Story Points Minimum"
min="0"
type="number"
value={agileCalculator.minPoints}
onInput={(e) =>
!Number.isNaN(e.currentTarget.valueAsNumber) &&
setAgileCalculator("minPoints", e.currentTarget.valueAsNumber)
}
/>
<TextInput
labelMinWidth="300px"
label="Story Points Maximum"
min="0"
type="number"
value={agileCalculator.maxPoints}
onInput={(e) =>
!Number.isNaN(e.currentTarget.valueAsNumber) &&
setAgileCalculator("maxPoints", e.currentTarget.valueAsNumber)
}
/>
<TextInput
labelMinWidth="300px"
label="Stunden pro Story Point"
suffix="h"
min="0"
type="number"
value={agileCalculator.hoursPerPoint}
onInput={(e) =>
!Number.isNaN(e.currentTarget.valueAsNumber) &&
setAgileCalculator("hoursPerPoint", e.currentTarget.valueAsNumber)
}
/>
<TextInput
labelMinWidth="300px"
label="Einzelpreis"
step="0.01"
suffix="CHF"
type="number"
value={agileCalculator.singlePrice}
onInput={(e) =>
!Number.isNaN(e.currentTarget.valueAsNumber) &&
setAgileCalculator("singlePrice", e.currentTarget.valueAsNumber)
}
/>
<div class="items-center grid grid-cols-[300px_1fr] gap-2">
<div class="px-1 font-mono text-sm text-right">
{"("}
<span title="Nichtrisiko Anteil" class="text-title-border">
{new Big(-100).plus(agileCalculator.risk).abs().toNumber()}%
</span>{" "}
*{" "}
<span title="Story Points Minimum" class="text-title-border">
{agileCalculator.minPoints} SP
</span>
{")"} + {"("}
<span title="Risiko Anteil" class="text-title-border">
{agileCalculator.risk}%
</span>{" "}
*{" "}
<span title="Story Points Maximum" class="text-title-border">
{agileCalculator.maxPoints > agileCalculator.minPoints
? agileCalculator.maxPoints
: agileCalculator.minPoints}{" "}
SP
</span>
{")"} =
</div>
<div class="px-1 flex justify-between">
<span>Gewichtete Story Points:</span>
{quantity()} SP
</div>
<div class="px-1 font-mono text-sm text-right">
<span title="Gewichtete Story Points" class="text-title-border">
{quantity()} SP
</span>{" "}
*{" "}
<span title="Stunden pro Story Point" class="text-title-border">
{agileCalculator.hoursPerPoint} h
</span>{" "}
=
</div>
<div class="px-1 flex justify-between">
<span>Menge:</span>
{hours()} h
</div>
<div class="px-1 font-mono text-sm text-right">
<span title="Menge" class="text-title-border">
{hours()} h
</span>{" "}
*{" "}
<span title="Einzelpreis" class="text-title-border">
{agileCalculator.singlePrice} CHF
</span>{" "}
=
</div>
<div class="px-1 font-bold flex justify-between">
<span>Gesamtpreis:</span>
{formatAmount(
new Big(hours()).mul(agileCalculator.singlePrice).toNumber()
)}{" "}
CHF
</div>
</div>
</div>
);
};
export default AgileCalculator;

@ -89,6 +89,7 @@ export const TextInput: Component<
[props.class || ""]: true, [props.class || ""]: true,
}} }}
type="text" type="text"
lang={rest.type === "number" ? "en" : undefined}
placeholder={props.placeholder} placeholder={props.placeholder}
{...rest} {...rest}
/> />

@ -13,7 +13,7 @@ import rehypeStringify from "rehype-stringify";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import HelpIcon from "~icons/carbon/help"; import HelpIcon from "~icons/carbon/help";
const markdownHelpUrl = export const markdownHelpUrl =
"https://drdanielappel.de/tipps-tools/markdown-eine-einfach-zu-erlernende-auszeichnungssprache/#cmtoc_anchor_id_0"; "https://drdanielappel.de/tipps-tools/markdown-eine-einfach-zu-erlernende-auszeichnungssprache/#cmtoc_anchor_id_0";
export const MarkdownHelpLabel: Component = () => ( export const MarkdownHelpLabel: Component = () => (

@ -12,26 +12,42 @@ import {
import Big from "big.js"; import Big from "big.js";
import Markdown from "./Markdown"; import Markdown from "./Markdown";
export const calculateAgileQuantity = (
hoursPerStoryPoint = 0,
riskFactor = 0,
minPoints = 0,
maxPoints = 0
) => {
if (minPoints > maxPoints) {
maxPoints = minPoints;
}
const minHours = minPoints * hoursPerStoryPoint;
const maxHours = maxPoints * hoursPerStoryPoint;
const minWeighted = new Big(-1).plus(riskFactor).abs().mul(minHours);
const maxWeighted = new Big(riskFactor).mul(maxHours);
const quantity = minWeighted.plus(maxWeighted).round().toNumber();
return quantity;
};
const getQuantity = (position: Position, state: StoreObject) => { const getQuantity = (position: Position, state: StoreObject) => {
let quantity = position.quantity; let quantity = position.quantity;
if (position.type === POSITION_TYPE_AGILE) { if (position.type === POSITION_TYPE_AGILE) {
const min = position.agilePointsMin || 0; const min = position.agilePointsMin || 0;
let max = position.agilePointsMax || 0; let max = position.agilePointsMax || 0;
if (min > max) {
max = min;
}
const minHours = min * state.agileHoursPerStoryPoint;
const maxHours = max * state.agileHoursPerStoryPoint;
const agileRiskFactor = const agileRiskFactor =
position.agileRiskFactor != null position.agileRiskFactor != null
? position.agileRiskFactor ? position.agileRiskFactor
: state.agileRiskFactor; : state.agileRiskFactor;
const normalizedRiskFactor = new Big(agileRiskFactor).mul(2).minus(1); quantity = calculateAgileQuantity(
const minWeighted = new Big(1).minus(normalizedRiskFactor).mul(minHours); state.agileHoursPerStoryPoint,
const maxWeighted = new Big(1).plus(normalizedRiskFactor).mul(maxHours); agileRiskFactor,
quantity = minWeighted.plus(maxWeighted).div(2).round().toNumber(); min,
max
);
} }
return quantity; return quantity;

@ -325,6 +325,8 @@ const SettingsOverlay: Component = () => {
required required
type="number" type="number"
label="Standard Einzelpreis" label="Standard Einzelpreis"
min="0"
step="0.01"
value={state.defaultItemPrice} value={state.defaultItemPrice}
onInput={(evt) => onInput={(evt) =>
setState( setState(
@ -332,6 +334,7 @@ const SettingsOverlay: Component = () => {
parseFloat(evt.currentTarget.value) || 0 parseFloat(evt.currentTarget.value) || 0
) )
} }
onBlur={resetInput(0)}
/> />
<div class="col-span-2"> <div class="col-span-2">

@ -22,8 +22,8 @@ import DeleteIcon from "~icons/carbon/trash-can";
import DragVerticalIcon from "~icons/carbon/drag-vertical"; import DragVerticalIcon from "~icons/carbon/drag-vertical";
import PositionSettingsIcon from "~icons/carbon/settings-adjust"; import PositionSettingsIcon from "~icons/carbon/settings-adjust";
import { Checkbox, TextArea, TextInput } from "../Form"; import { Checkbox, TextArea, TextInput } from "../Form";
import { parseOptionalFloat } from "~/util";
import { MarkdownHelpLabel } from "../Markdown"; import { MarkdownHelpLabel } from "../Markdown";
import { createOptionalNumberInputHandler } from "~/util";
export const PositionsSettings: Component = () => { export const PositionsSettings: Component = () => {
const [state, setState] = useContext(StoreContext)!; const [state, setState] = useContext(StoreContext)!;
@ -295,25 +295,19 @@ export const PositionsSettings: Component = () => {
</div> </div>
<TextInput <TextInput
size="xs" size="xs"
value={ value={position.itemPrice}
position.itemPrice === 0 ? "" : position.itemPrice
}
label="Einzelpreis" label="Einzelpreis"
placeholder={ placeholder={
state.defaultItemPrice state.defaultItemPrice
? state.defaultItemPrice + "" ? state.defaultItemPrice + ""
: undefined : undefined
} }
step="0.01"
min="0" min="0"
type="number" type="number"
onInput={(e) => { onInput={createOptionalNumberInputHandler((v) =>
setState( setState("positions", idx(), "itemPrice", v)
"positions", )}
idx(),
"itemPrice",
parseOptionalFloat(e.currentTarget.value)
);
}}
/> />
<div <div
use:autoAnimate use:autoAnimate
@ -356,16 +350,19 @@ export const PositionsSettings: Component = () => {
<div class="col-span-2"> <div class="col-span-2">
<TextInput <TextInput
label="Aktionspreis" label="Aktionspreis"
suffix="CHF"
type="number" type="number"
step="0.01"
min="0"
value={position.fixedDiscountPrice} value={position.fixedDiscountPrice}
onInput={(e) => onInput={createOptionalNumberInputHandler((v) =>
setState( setState(
"positions", "positions",
idx(), idx(),
"fixedDiscountPrice", "fixedDiscountPrice",
parseOptionalFloat(e.currentTarget.value) v
) )
} )}
/> />
</div> </div>
<div class="col-span-2"> <div class="col-span-2">

@ -2,9 +2,12 @@ import {
Component, Component,
createEffect, createEffect,
createMemo, createMemo,
FlowComponent,
onCleanup, onCleanup,
onMount, onMount,
JSX,
useContext, useContext,
splitProps,
} from "solid-js"; } from "solid-js";
import LufraiLogo from "~icons/custom/lufrai-logo"; import LufraiLogo from "~icons/custom/lufrai-logo";
import AppIcon from "~icons/custom/icon"; import AppIcon from "~icons/custom/icon";
@ -33,14 +36,30 @@ import {
shuffle, shuffle,
} from "~/util"; } from "~/util";
import { capitalize } from "froebel"; import { capitalize } from "froebel";
import { markdownHelpUrl } from "./Markdown";
import AgileCalculator from "./AgileCalculator";
export const description = 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."; "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";
const ExternalLink: FlowComponent<
JSX.AnchorHTMLAttributes<HTMLAnchorElement>
> = (p) => {
const [props, rest] = splitProps(p, ["children"]);
return (
<a class={externalLinkClass} {...externalLink} {...rest}>
{props.children}
<ExternalLinkIcon />
</a>
);
};
const WelcomeModal: Component = (props) => { const WelcomeModal: Component = (props) => {
const [localState, setLocalState, localStateMounted] = const [localState, setLocalState, localStateMounted] =
useContext(LocalStoreContext)!; useContext(LocalStoreContext)!;
const [AcordionItem] = createAccordion(); const [AccordionItem] = createAccordion();
let subtitleEl: HTMLSpanElement = undefined!; let subtitleEl: HTMLSpanElement = undefined!;
const isOpen = createMemo(() => { const isOpen = createMemo(() => {
return localStateMounted() && localState.showWelcome; return localStateMounted() && localState.showWelcome;
@ -134,7 +153,7 @@ const WelcomeModal: Component = (props) => {
<> <>
Fredi Niklaus ( Fredi Niklaus (
<a <a
class="inline-flex items-center gap-1" class={externalLinkClass}
href="https://www.remedyit.ch/" href="https://www.remedyit.ch/"
{...externalLink} {...externalLink}
> >
@ -229,7 +248,10 @@ const WelcomeModal: Component = (props) => {
</a> </a>
</div> </div>
<div class="mt-16 flex flex-wrap gap-3 items-center justify-center"> <div
class="mt-16 flex flex-wrap gap-3 items-center justify-center"
id="welcome-social"
>
<div class="text-sm text-slate-600">Teile es auf:</div> <div class="text-sm text-slate-600">Teile es auf:</div>
<div <div
class="shareon flex flex-wrap items-center justify-center" class="shareon flex flex-wrap items-center justify-center"
@ -304,13 +326,40 @@ const WelcomeModal: Component = (props) => {
Wieso ist <span class="text-swiss-red">Räppli</span> komplett{" "} Wieso ist <span class="text-swiss-red">Räppli</span> komplett{" "}
<span class="text-swiss-red">kostenlos</span>? <span class="text-swiss-red">kostenlos</span>?
</h2> </h2>
<p class="text-xl font-light lead">
Weil moderne Rechnungsstellung für alle Menschen möglich sein
sollte!
</p>
<p> <p>
Consectetur id magna labore commodo exercitation laboris est Jeder Selbstständige kommt wohl irgendwann an den Punkt, dass er
laboris consectetur irure minim. Officia anim tempor adipisicing eine Rechnung schreiben muss. Doch eine moderne Rechnung zu
irure labore tempor reprehenderit culpa consequat ea esse schreiben, die heutige Erwartungen erfüllt, ist gar nicht so
exercitation. Consectetur labore velit nulla excepteur eiusmod sit einfach und braucht viel Zeit. So werden viele Selbstständige zum
fugiat do proident. Ex non consectetur mollit dolor dolore Lorem Einsatz von komplexer und oftmals teurer "ERP"-Software oder zu
ut et incididunt pariatur sunt deserunt tempor magna ullamco. Outsourcing gedrängt. Menschen die sich mit wenig Startkapital
selbstständig machen wollen, stehen vor einer schwierigen Wahl.
</p>
<p>
Eines der wesentlichen Ziele von{" "}
<a href="#welcome-lufrai">Lufrai</a> ist, Menschen dabei zu
unterstützen aus eigener Kraft unabhängig zu werden und ihrem
eigenen Lebenssinn zu folgen.{" "}
<strong>
<i>Befreie deine Stimme!</i>
</strong>
</p>
<p>
Räppli soll ein Beweis dafür darstellen, dass Selbstlosigkeit auch
ohne teure, zentralisierte, staatliche Lösungen möglich ist. Der
gesamte Quellcode von Räppli steht offen verfügbar.{" "}
<a href="#welcome-opensource">Erfahre mehr</a>
</p>
<p>
Zu guter Letzt ist Räppli auch eine Referenzarbeit. Sie soll
zeigen was ich (Katja) als Web-Entwicklerin drauf habe. Denn im
mündlichen Gespräch verkaufe ich meine Fähigkeiten leider wie eine
absolute Niete und wirke mit einer Grösse von 1.9 Metern etwas
abschreckend. Verkauf ist nicht meine Stärke.
</p> </p>
</section> </section>
@ -321,7 +370,7 @@ const WelcomeModal: Component = (props) => {
<p> <p>
Lufrai ist die Einzelfirma von{" "} Lufrai ist die Einzelfirma von{" "}
<a <a
class="inline-flex items-center gap-2" class={externalLinkClass}
href="https://lufrai.org/katjalutz" href="https://lufrai.org/katjalutz"
{...externalLink} {...externalLink}
> >
@ -331,7 +380,7 @@ const WelcomeModal: Component = (props) => {
für eine unabhängige, freie Schweiz ein und unterstützt ihre für eine unabhängige, freie Schweiz ein und unterstützt ihre
souveränen Mitmenschen, das Steuer selbst in die Hand zu nehmen. souveränen Mitmenschen, das Steuer selbst in die Hand zu nehmen.
<a <a
class="btn btn-sm inline-flex ml-3 items-center gap-2" class="btn btn-sm inline-flex ml-3 items-center gap-1"
href="https://lufrai.org" href="https://lufrai.org"
{...externalLink} {...externalLink}
> >
@ -379,24 +428,47 @@ const WelcomeModal: Component = (props) => {
<section class="mt-16"> <section class="mt-16">
<h2 id="welcome-patron">Das Projekt unterstützen</h2> <h2 id="welcome-patron">Das Projekt unterstützen</h2>
<p class="text-xl font-light lead">
Danke dass du das Projekt und damit auch den Fortbestand von
Lufrai unterstützen möchtest!
</p>
<h3>Teilen</h3>
<p>
<a href="#welcome-social">Teile</a> Räppi unter deinen Freunden
und Bekannten was das Zeug hält! Lufrai investiert kein Geld in
teures Marketing, aus der Überzeugung, dass es auf unserer Welt
bereits genug davon gibt.
</p>
<h3>Spenden</h3>
<p> <p>
Irure laboris quis consequat enim tempor dolor. Esse velit Räppli 1.0 wurde mit einem Aufwand von fast 4 Wochen entwickelt, "
occaecat dolore aute cillum pariatur reprehenderit irure duis eu <a href="#welcome-lufrai">Lufrai - Katja Lutz</a>" hat somit etwa
nulla pariatur fugiat consectetur ipsum. Commodo ad aliquip nulla 18'000 CHF in die Verwirklichung von diesem Projekt investiert.{" "}
non incididunt. Officia veniam cillum cillum. Velit ex aliquip <strong>
mollit deserunt nostrud id amet voluptate ea duis cupidatat Herzlichen Dank, dass du dich daran beteiligen möchtest!
officia culpa consequat enim. Voluptate anim fugiat anim et elit </strong>
aute cupidatat. Duis aliquip laboris adipisicing dolore elit </p>
voluptate proident occaecat excepteur culpa exercitation velit. <a
Veniam esse quis voluptate aliquip culpa. Aute cupidatat sunt amet href="https://lufrai.org/spenden/"
ad fugiat id elit. Officia proident ea deserunt quis anim elit class={`btn btn-sm btn-secondary bg-purple-600 border-purple-600 ${externalLinkClass}`}
cupidatat pariatur. Aliqua dolore ad eiusmod qui eu ea enim qui {...externalLink}
enim incididunt aute irure tempor fugiat sunt. Consequat magna >
culpa Lorem nostrud cillum eu cillum adipisicing eu aute duis Kontoinformationen <ExternalLinkIcon />
excepteur. In anim mollit amet elit ex. </a>
<h3>Mitwirkung / Feedback</h3>
<p>
Für Feedbacks, bitte beachte den entsprechenden{" "}
<a href="#welcome-feedback" onClick={onClickFocus}>
FAQ-Abschnitt
</a>
.
</p>
<p>
Du bist ein Web-Entwickler, verstehst Javascript / Typescript wie
deine Westentasche und möchtest Räppli verbessern? Hurra! Bitte
schaue dir den <a href="#welcome-opensource">Open Source</a>{" "}
Abschnitt an.
</p> </p>
<h3>Spende</h3>
<h3>Mitwirkung</h3>
</section> </section>
<section class="mt-16"> <section class="mt-16">
@ -406,16 +478,12 @@ const WelcomeModal: Component = (props) => {
kopieren, verteilen und erweitern. Du brauchst also keine Angst zu kopieren, verteilen und erweitern. Du brauchst also keine Angst zu
haben, dass du irgendwann keine Rechnungen mehr schreiben darfst. haben, dass du irgendwann keine Rechnungen mehr schreiben darfst.
Lizenziert ist Räppli mit{" "} Lizenziert ist Räppli mit{" "}
<a <ExternalLink href="https://git.lufrai.com/rappli/rappli/src/branch/master/LICENSE">
class="inline-flex items-center gap-1"
href="https://git.lufrai.com/rappli/rappli/raw/branch/master/LICENSE"
>
MIT MIT
<ExternalLinkIcon /> </ExternalLink>
</a>
. .
<a <a
class="btn btn-sm inline-flex ml-3 items-center gap-2" class="btn btn-sm inline-flex ml-3 items-center gap-1"
href="https://git.lufrai.com/rappli/rappli" href="https://git.lufrai.com/rappli/rappli"
{...externalLink} {...externalLink}
> >
@ -436,7 +504,7 @@ const WelcomeModal: Component = (props) => {
<ul> <ul>
<li> <li>
<a <a
class="flex items-center gap-2" class={externalLinkClass}
href="https://www.solidjs.com/" href="https://www.solidjs.com/"
{...externalLink} {...externalLink}
> >
@ -446,7 +514,7 @@ const WelcomeModal: Component = (props) => {
</li> </li>
<li> <li>
<a <a
class="flex items-center gap-2" class={externalLinkClass}
href="https://tailwindcss.com/" href="https://tailwindcss.com/"
{...externalLink} {...externalLink}
> >
@ -456,7 +524,7 @@ const WelcomeModal: Component = (props) => {
</li> </li>
<li> <li>
<a <a
class="flex items-center gap-2" class={externalLinkClass}
href="https://daisyui.com/" href="https://daisyui.com/"
{...externalLink} {...externalLink}
> >
@ -466,7 +534,7 @@ const WelcomeModal: Component = (props) => {
</li> </li>
<li> <li>
<a <a
class="flex items-center gap-2" class={externalLinkClass}
href="https://sortablejs.github.io/Sortable/" href="https://sortablejs.github.io/Sortable/"
{...externalLink} {...externalLink}
> >
@ -476,7 +544,7 @@ const WelcomeModal: Component = (props) => {
</li> </li>
<li> <li>
<a <a
class="flex items-center gap-2" class={externalLinkClass}
href="https://remark.js.org/" href="https://remark.js.org/"
{...externalLink} {...externalLink}
> >
@ -490,30 +558,47 @@ const WelcomeModal: Component = (props) => {
<section class="mt-16"> <section class="mt-16">
<h2 id="welcome-thankyou">Danksagungen</h2> <h2 id="welcome-thankyou">Danksagungen</h2>
<p> <p>
An dieser Stelle möchte ich mich bei der Community von{" "} An dieser Stelle möchte ich (Katja) mich bei der Community von{" "}
<a <a
class="inline-flex items-center gap-1 mr-1" class={externalLinkClass}
href="https://www.solidjs.com/" href="https://www.solidjs.com/"
{...externalLink} {...externalLink}
> >
Solid <ExternalLinkIcon /> Solid <ExternalLinkIcon />
</a> </a>{" "}
bedanken, nicht nur für ihr verblüffend effektives Open Source bedanken, nicht nur für ihr verblüffend effektives Open Source
Web-Framework, sondern auch für ihre Offenheit, Anregungen und Web-Framework, sondern auch für ihre Offenheit, Anregungen und
cleveren Tipps! cleveren Tipps!
</p> </p>
<p> <p>
Ausserdem möchte ich (Katja) mich insbesondere bei{" "} Ausserdem möchte ich mich insbesondere bei {thankYouRahelAndFredi}{" "}
{thankYouRahelAndFredi} herzlichst bedanken, welche mir stets bei herzlichst bedanken, welche mir stets bei der Umsetzung meiner
der Umsetzung meiner kurligen Projekte unterstützend zur Seite kurligen Projekte unterstützend zur Seite standen und mich immer
standen und mich immer inspirierten, einen Schritt weiterzugehen! inspirierten, einen Schritt weiterzugehen!
</p>
<p>
Mein herzlicher Dank gilt zudem auch{" "}
<ExternalLink href="https://www.kohei.dev/en-us">
Kohei Asai
</ExternalLink>{" "}
<small>
(
<ExternalLink href="https://twitter.com/axross_">
Twitter
</ExternalLink>
)
</small>
, welcher mir sehr grosszügig seine Rechte am Paketnamen "rappli"
auf{" "}
<ExternalLink href="https://npmjs.com">npmjs.com</ExternalLink>{" "}
übertragen hat.
</p> </p>
</section> </section>
<section class="mt-16"> <section class="mt-16">
<h2 id="welcome-faq">Häufig gestellte Fragen</h2> <h2 id="welcome-faq">Häufig gestellte Fragen</h2>
<div class="text-black"> <div class="text-black">
<AcordionItem <AccordionItem
id="welcome-privacy" id="welcome-privacy"
label={"1. Wo werden welche Daten gespeichert?"} label={"1. Wo werden welche Daten gespeichert?"}
alignCenter={false} alignCenter={false}
@ -526,7 +611,7 @@ const WelcomeModal: Component = (props) => {
Einige deiner Daten, wie z.B. Bankverbindung, Logo und Einige deiner Daten, wie z.B. Bankverbindung, Logo und
Zahlungsbedingungen verbleiben im{" "} Zahlungsbedingungen verbleiben im{" "}
<span <span
class="border-b border-black border-dotted" class="text-title-border"
title="Der sogennante localStorage" title="Der sogennante localStorage"
> >
Speicher Speicher
@ -539,7 +624,7 @@ const WelcomeModal: Component = (props) => {
funktionieren, müssen jedoch unweigerlich einige Daten funktionieren, müssen jedoch unweigerlich einige Daten
übertragen werden, wie z.B. deine IP-Adresse. Erfahre{" "} übertragen werden, wie z.B. deine IP-Adresse. Erfahre{" "}
<a <a
class="inline-flex items-center gap-1" class={externalLinkClass}
href="https://t3n.de/news/tcp-ip-internet-grundlagen-755667/" href="https://t3n.de/news/tcp-ip-internet-grundlagen-755667/"
{...externalLink} {...externalLink}
> >
@ -549,46 +634,134 @@ const WelcomeModal: Component = (props) => {
wie das Internet funktioniert. wie das Internet funktioniert.
</li> </li>
</ul> </ul>
</AcordionItem> </AccordionItem>
<AcordionItem <AccordionItem
label="2. Ich habe vergessen zu speichern, was nun?!" label="2. Ich habe vergessen zu speichern, was nun?!"
alignCenter={false} alignCenter={false}
> >
Deine Daten werden auf keinem Server gespeichert. Lerne aus dem Deine Daten werden auf keinem Server gespeichert. Lerne aus dem
Malheur und speichere ab sofort regelmässig{" "} Malheur und speichere ab sofort regelmässig{" "}
<HugIcon class="inline" />! <HugIcon class="inline" />!
</AcordionItem> </AccordionItem>
<AcordionItem <AccordionItem label="3. Werden meine Nutzungsdaten zu Marketingzwecken gesammelt?">
<strong>Nein</strong>, vorausgesetzt du verwendest das
offizielle Räppli über rappli.ch und keine modizifierte Version.
Lufrai legt Wert auf deine Privatsphäre. Es werden keine
Nutzungsdaten an Google, Facebook oder ähnliche Riesen
übermittelt.
</AccordionItem>
<AccordionItem
id="welcome-feedback"
label={ label={
<> <>
3. Ich habe einen Anpassungswunsch! 4. Ich habe einen Anpassungswunsch!
<br /> <br />
Kann die App bitte gratis angepasst werden? Kann die App bitte gratis angepasst werden?
</> </>
} }
alignCenter={false} alignCenter={false}
> >
test Räppli ist <a href="#welcome-opensource">Open Source</a> und
</AcordionItem> wird grundsätzlich von <a href="#welcome-lufrai">Lufrai</a>{" "}
weiterentwickelt. Für eine optimale Weiterentwicklung ist dein
Feedback sehr wichtig und ich (Katja) bin für dein Feedback und
deine Ideen dankbar! Jedoch besteht keine Garantie dafür, dass
jedes Feedback beantwortet / umgesetzt werden kann, die
Entwicklung ist aufwändig und kein Wunschkonzert. Kontaktiere
Lufrai auf{" "}
<a
href="https://lufrai.org/impressum/"
class={externalLinkClass}
{...externalLink}
>
Kontakt <ExternalLinkIcon />
</a>{" "}
oder erstelle ein Issue auf dem{" "}
<a href="#welcome-opensource">Git-Repository</a>.
</AccordionItem>
<AcordionItem <AccordionItem
id="welcome-agile" id="welcome-agile"
label={ label={
<div> <div>
4. Wie können Aufwand-Schätzungen gemacht werden? 5. Wie funktionieren <strong>Agile Positionen</strong>?
<br />
Was hat es mit <strong>Agile</strong> auf sich?
</div> </div>
} }
alignCenter={false} alignCenter={false}
> >
test <p class="!mt-0 text-lg font-light lead">
</AcordionItem> Agile Positionen können dir dabei helfen, Arbeitsaufwand
(Zeit) zu schätzen.
</p>
<p>
Der Preis von agilen Positionen wird aus Story Points, einem
Umrechnungssatz "Stunden pro Story Point", dem Risiko Faktor
und dem Stundensatz ("Einzelpreis") berechnet.
</p>
<h3>Was sind Story Points?</h3>
<p>
Story Points sind relative Punktzahlen. Weise all deinen
Positionen Story Points zu, die im Verhältnis zueinander
stimmen. Vergleiche dazu deine Positionen und überlege dir,
welche davon schwieriger sind und welche einfacher. Wie viel
schwieriger ist eine Position (Arbeitsaufwand) im Vergleich zu
einer anderen?
</p>
<p>
Nachdem du die Story Points zugewiesen hast, kannst du dir
überlegen, wie viel Stunden ein einzelner Story Point wert
ist, trage diesen in den "Dokument"-Einstellungen unter
"Stunden pro Story Point" ein.
</p>
<a
href="https://www.agile-academy.com/de/product-owner/was-sind-story-points/"
class={externalLinkClass}
{...externalLink}
>
Erfahre mehr über Story Points
<ExternalLinkIcon />
</a>
<h3>Was ist der Risiko Faktor?</h3>
<p>
Aufwandschätzungen sind eine riskante Angelegenheit. Räppli
erlaubt dir deshalb für den geschätzten, optimalen Fall "Story
Points Minimum" und für den schlimmsten Fall "Story Points
Maximum" anzugeben.
<br />
<strong>
Der Risiko Faktor wird in den "Dokument"-Einstellungen
eingegeben und er entscheidet über die Gewichtung der
minimalen und maximalen Story Points:
</strong>
</p>
<ul class="mb-8">
<li>
Risiko Faktor von <span class="underline">0%</span>: Es
existiert kein Risiko, <strong>nur die minimalen</strong>{" "}
Story Points werden beachtet!
</li>
<li>
Risiko Faktor von <span class="underline">70%</span>:{" "}
<strong>30% von den Minimalen</strong>, und{" "}
<strong>70% von den maximalen</strong> Story Points werden
beachtet.
</li>
<li>
Risiko Faktor von <span class="underline">100%</span>: Der
Worst-Case ist alternativlos,{" "}
<strong>nur die maximalen</strong> Story Points werden
beachtet!
</li>
</ul>
<AgileCalculator />
</AccordionItem>
<AcordionItem <AccordionItem
label={"5. Welche Geräte werden unterstützt?"} label={"6. Welche Geräte werden unterstützt?"}
alignCenter={false} alignCenter={false}
> >
<p class="!mt-0"> <p class="!mt-0">
@ -598,33 +771,115 @@ const WelcomeModal: Component = (props) => {
erreicht, jedoch sind auch "Google Chrome" basierte Browser erreicht, jedoch sind auch "Google Chrome" basierte Browser
geeignet. Verwende einen aktuellen Browser! geeignet. Verwende einen aktuellen Browser!
</p> </p>
</AcordionItem> </AccordionItem>
<AcordionItem <AccordionItem
label={ label={
<div> <div>
6. Können Fliesstexte formatiert werden? 7. Können Fliesstexte formatiert werden?
<br /> <br />
Was ist <strong>Markdown</strong>? Was ist <strong>Markdown</strong>?
</div> </div>
} }
alignCenter={false} alignCenter={false}
> >
test <p class="!mt-0 mb-1">
</AcordionItem> Du kannst deinen Text formatieren, indem du ihn mit ganz
bestimmten Zeichen versiehst! Welche Zeichen das sind, wird
durch den "Markdown"-Standard definiert.
</p>
<p class="mt-1">
Erfahre{" "}
<a
href={markdownHelpUrl}
class={externalLinkClass}
{...externalLink}
>
hier <ExternalLinkIcon />
</a>{" "}
wie Markdown im Detail funktioniert!
</p>
<AcordionItem <strong>Kurzbeispiel:</strong>
<div class="prose-sm">
<div class="grid grid-cols-2">
<p>Dein Text:</p>
<pre>
{["- *Pizza*", "- **Gemüse**", "- ***Salat***"].join(
"\n"
)}
</pre>
<p>Ausgabe:</p>
<div class="border border-slate-200">
<ul>
<li>
<i>Pizza</i>
</li>
<li>
<strong>Gemüse</strong>
</li>
<li>
<i>
<strong>Salat</strong>
</i>
</li>
</ul>
</div>
</div>
</div>
</AccordionItem>
<AccordionItem
id="welcome-local-installation" id="welcome-local-installation"
label={<div>7. Kann Räppli lokal installiert werden?</div>} label={<div>8. Kann Räppli lokal installiert werden?</div>}
alignCenter={false} alignCenter={false}
> >
test <p class="!mt-0">
</AcordionItem> Räppli kann lokal installiert werden, jedoch ist es für den
Betrieb auf Servern optimiert. Das heisst: auch wenn du Räppli
installiert hast, musst du deinen Webbrowser nutzen, um Räppli
zu öffnen.
</p>
<p class="bg-slate-500/10 flex p-3 items-center gap-3 rounded">
<WarningIcon class="w-auto h-6 inline mr-1" />{" "}
<strong>
Für die Installation sind grundlegende Kenntnisse im Umgang
mit der Kommandozeile empfohlen!
</strong>
</p>
<h3>Voraussetzungen:</h3>
<ul>
<li>
Installiere{" "}
<a
class={externalLinkClass}
href="https://nodejs.org/en/"
{...externalLink}
>
nodejs {">"}= 18 <ExternalLinkIcon />
</a>
</li>
</ul>
<h3>Installation Räppli</h3>
<p>Führe folgenden Befehl aus:</p>
<pre>npm install -g rappli</pre>
<h3>Räppli starten</h3>
<p>Führe folgenden Befehl aus, um Räppli zu starten</p>
<pre>rappli</pre>
<p>
Sobald die Meldung "Listening on port 3000" erscheint, kannst
du dein lokales Räppli auf deinem Browser über{" "}
<a href="http://localhost:3000" {...externalLink}>
http://localhost:3000
</a>{" "}
erreichen.
</p>
</AccordionItem>
<AcordionItem <AccordionItem
label={ label={
<div> <div>
8. Wie präzise wurden die SIX QR-Rechnung Vorgaben 9. Wie präzise wurden die SIX QR-Rechnung Vorgaben
umgesetzt? umgesetzt?
</div> </div>
} }
@ -640,10 +895,10 @@ const WelcomeModal: Component = (props) => {
Mittel der Schweizer Regierung zur Verfügung standen, wurde Mittel der Schweizer Regierung zur Verfügung standen, wurde
das Rad nicht komplett neu erfunden. das Rad nicht komplett neu erfunden.
</p> </p>
</AcordionItem> </AccordionItem>
<AcordionItem <AccordionItem
label={<div>9. Haftungsausschluss</div>} label={<div>10. Haftungsausschluss</div>}
alignCenter={false} alignCenter={false}
> >
<p class="!mt-0"> <p class="!mt-0">
@ -662,7 +917,7 @@ const WelcomeModal: Component = (props) => {
</a> </a>
. .
</p> </p>
</AcordionItem> </AccordionItem>
</div> </div>
</section> </section>
</div> </div>
@ -707,10 +962,14 @@ const WelcomeModal: Component = (props) => {
<WarningIcon /> Bitte verwende einen Laptop oder Computer! <WarningIcon /> Bitte verwende einen Laptop oder Computer!
</div> </div>
</div> </div>
<div class="absolute bottom-8 right-8 text-xs text-black opacity-60 flex gap-3"> <a
href="https://git.lufrai.com/rappli/rappli/src/branch/master/CHANGELOG.md#changelog"
class="hover:underline absolute bottom-8 right-8 text-xs text-black opacity-60 flex gap-3"
{...externalLink}
>
<span>Version: {__APP_VERSION__}</span> <span>Version: {__APP_VERSION__}</span>
<span>{getDisplayDate(new Date(__BUILD_TIME__))}</span> <span>{getDisplayDate(new Date(__BUILD_TIME__))}</span>
</div> </a>
</Modal> </Modal>
); );
}; };

@ -7,6 +7,10 @@
hyphens: auto; hyphens: auto;
} }
.text-title-border {
@apply border-b border-black border-dotted;
}
.text-height { .text-height {
height: 1em; height: 1em;
width: auto; width: auto;

@ -5,13 +5,6 @@ import { JSX } from "solid-js";
export const sleep = (timeout: number) => export const sleep = (timeout: number) =>
new Promise((res) => setTimeout(res, timeout)); new Promise((res) => setTimeout(res, timeout));
export const parseOptionalFloat = (text: string) => {
const result = parseFloat(text);
if (Number.isNaN(result)) return undefined;
return result;
};
// Source: https://stackoverflow.com/a/34591063 // Source: https://stackoverflow.com/a/34591063
export const roundToStep = (value: number, step = 1.0) => { export const roundToStep = (value: number, step = 1.0) => {
const inv = new Big(1.0).div(step); const inv = new Big(1.0).div(step);
@ -88,3 +81,18 @@ export const onClickFocus: JSX.EventHandlerUnion<
}; };
export const externalLink = { target: "_blank", rel: "noopener" }; export const externalLink = { target: "_blank", rel: "noopener" };
export const createOptionalNumberInputHandler = (
onInput: (v: number | undefined) => void
) => {
return (e: InputEvent & { currentTarget: HTMLInputElement }) => {
if (e.currentTarget.validity.badInput) {
return;
}
const value =
e.currentTarget.value == "" ? undefined : e.currentTarget.valueAsNumber;
onInput(value);
};
};

Loading…
Cancel
Save