diff --git a/.gitignore b/.gitignore
index 8f09412..38c7387 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@ dist
.vercel
.netlify
netlify
+/data.json
# dependencies
/node_modules
diff --git a/.npmignore b/.npmignore
index 56bf485..a388e11 100644
--- a/.npmignore
+++ b/.npmignore
@@ -1,3 +1,5 @@
+# Storage
+/data.json
# IDEs and editors
/.idea
diff --git a/package-lock.json b/package-lock.json
index 14acfbb..7340226 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
"remark-rehype": "^10.1.0",
"shareon": "^2.0.0",
"solid-app-router": "^0.4.1",
+ "solid-confetti-explosion": "^1.0.3",
"solid-js": "^1.4.8",
"solid-meta": "^0.27.5",
"sortablejs": "^1.15.0",
@@ -2926,6 +2927,13 @@
"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.20.0",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.20.0.tgz",
@@ -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",
@@ -6932,6 +6950,16 @@
"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.8",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.8.tgz",
@@ -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",
@@ -10254,6 +10296,13 @@
"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.20.0",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-2.20.0.tgz",
@@ -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",
@@ -12975,6 +13032,13 @@
"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.8",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.8.tgz",
@@ -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",
diff --git a/package.json b/package.json
index f263f85..e2cdd06 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"remark-rehype": "^10.1.0",
"shareon": "^2.0.0",
"solid-app-router": "^0.4.1",
+ "solid-confetti-explosion": "^1.0.3",
"solid-js": "^1.4.8",
"solid-meta": "^0.27.5",
"sortablejs": "^1.15.0",
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx
index e72ad29..883d20c 100644
--- a/src/components/Modal.tsx
+++ b/src/components/Modal.tsx
@@ -20,7 +20,7 @@ const Modal: FlowComponent<{ open: boolean }> = (props) => {
"!visible": isVisible(),
}}
>
-
diff --git a/src/components/WelcomeModal.tsx b/src/components/WelcomeModal.tsx
index 6e479cf..4230174 100644
--- a/src/components/WelcomeModal.tsx
+++ b/src/components/WelcomeModal.tsx
@@ -10,13 +10,20 @@ import {
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";
@@ -38,14 +45,18 @@ import {
getHost,
onClickFocus,
} from "~/util";
-import { capitalize, shuffle } 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
@@ -68,6 +79,8 @@ const WelcomeModal: Component = (props) => {
return localStateMounted() && localState.showWelcome;
});
const [showTrailer, setShowTrailer] = createSignal(false);
+ const increaseCount_ = server(increaseLikes);
+ const [likes] = createResource(server(getLikes));
onMount(function () {
let adjectives = [
@@ -168,6 +181,29 @@ 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 (
@@ -953,10 +989,10 @@ const WelcomeModal: Component = (props) => {
-
+
+
+
-
- Bitte verwende einen Laptop oder Computer mit einer
- Mindestbreite von 1024 Pixeln!
-
)
diff --git a/src/server/likes.ts b/src/server/likes.ts
new file mode 100644
index 0000000..36800ef
--- /dev/null
+++ b/src/server/likes.ts
@@ -0,0 +1,8 @@
+import { state } from "./store";
+
+export const increaseLikes = () => {
+ state().likes++;
+};
+export const getLikes = () => {
+ return state().likes;
+};
diff --git a/src/server/store.ts b/src/server/store.ts
new file mode 100644
index 0000000..27b3267
--- /dev/null
+++ b/src/server/store.ts
@@ -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 = (
+ 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,
+}));
diff --git a/vite.config.ts b/vite.config.ts
index 9d522d7..92875cb 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -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()),