From 0d598641217f0968adb63799be5fa68100d3de68 Mon Sep 17 00:00:00 2001 From: uc-hoba Date: Wed, 10 Jun 2026 10:38:58 +0800 Subject: [PATCH] feat(store): add persistent settings store with zustand --- bun.lock | 15 ++++++++++++++- next.config.ts | 21 ++++++--------------- package.json | 5 ++++- src/stores/settings-store.ts | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 src/stores/settings-store.ts diff --git a/bun.lock b/bun.lock index 250acdd..e83c55f 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@serwist/next": "^9.5.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.4.0", "lucide-react": "^1.17.0", "next": "16.2.9", "react": "19.2.4", @@ -16,6 +17,8 @@ "shadcn": "^4.11.0", "tailwind-merge": "^3.6.0", "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14", }, "devDependencies": { "@biomejs/biome": "2.2.0", @@ -361,6 +364,8 @@ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], @@ -839,10 +844,12 @@ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], - "zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -857,6 +864,12 @@ "@modelcontextprotocol/sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@serwist/build/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@serwist/next/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + + "@serwist/webpack-plugin/zod": ["zod@4.4.1", "", {}, "sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg=="], diff --git a/next.config.ts b/next.config.ts index 728b584..3b0f058 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,24 +1,15 @@ -import { spawnSync } from 'node:child_process'; import withSerwistInit from '@serwist/next'; - -// This is optional! -// A revision helps Serwist version a precached page. This -// avoids outdated precached responses being used. Using -// `git rev-parse HEAD` might not the most efficient way -// of determining a revision, however. You may prefer to use -// the hashes of every extra file you precache. -const revision = - spawnSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf-8' }).stdout ?? - crypto.randomUUID(); +import { format } from 'date-fns'; const withSerwist = withSerwistInit({ - additionalPrecacheEntries: [{ url: '/~offline', revision }], - // Note: This is only an example. If you use Pages Router, - // use something else that works, such as "service-worker/index.ts". + additionalPrecacheEntries: [{ url: '/~offline' }], swSrc: 'app/sw.ts', swDest: 'public/sw.js', }); export default withSerwist({ - // Your Next.js config + env: { + APP_VERSION: `Ver. ${format(new Date(), 'yy.MM.dd-HHmm')}`, + }, + output: 'standalone', }); diff --git a/package.json b/package.json index ebd53b3..b9a7cf3 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,16 @@ "@serwist/next": "^9.5.11", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.4.0", "lucide-react": "^1.17.0", "next": "16.2.9", "react": "19.2.4", "react-dom": "19.2.4", "shadcn": "^4.11.0", "tailwind-merge": "^3.6.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { "@biomejs/biome": "2.2.0", diff --git a/src/stores/settings-store.ts b/src/stores/settings-store.ts new file mode 100644 index 0000000..e1fd956 --- /dev/null +++ b/src/stores/settings-store.ts @@ -0,0 +1,32 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +type SettingsState = { + hasHydrated: boolean; + setHasHydrated: (v: boolean) => void; + devMode: boolean; + setDevMode: (mode: boolean) => void; + id: string; + setId: (id: string) => void; +}; + +export const useSettingsStore = create()( + persist( + (set) => ({ + hasHydrated: false, + setHasHydrated: (v) => set({ hasHydrated: v }), + devMode: false, + setDevMode: (mode) => set({ devMode: mode }), + id: 'new-client', + setId: (id) => set({ id }), + }), + { + name: 'settings-storage', + onRehydrateStorage: () => (state) => { + if (state) { + state.setHasHydrated(true); + } + }, + }, + ), +);