From 6c801637f6a3131ec10892662c9269a1130714c8 Mon Sep 17 00:00:00 2001 From: Katja Lutz Date: Wed, 27 Jul 2022 22:42:30 +0200 Subject: [PATCH] feat: implement like button closes issue #18 --- .gitignore | 1 + .npmignore | 2 + package-lock.json | 75 +++++++++++++++++++++ package.json | 1 + src/components/Modal.tsx | 2 +- src/components/WelcomeModal.tsx | 112 +++++++++++++++++++++++++++----- src/entry-server.tsx | 3 + src/server/likes.ts | 8 +++ src/server/store.ts | 64 ++++++++++++++++++ vite.config.ts | 3 + 10 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/server/likes.ts create mode 100644 src/server/store.ts 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) => { - ) 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()),