11import { create } from 'zustand' ;
2- import { persist } from 'zustand/middleware' ;
2+ import { persist , createJSONStorage } from 'zustand/middleware' ;
33import type { BillState , PersonalInfo , VoucherConfig , BillSide , HourValue , Language } from '../types/bill' ;
4- import { resizeImageForStorage } from '../services/imageEffects' ;
54import { clearHueShiftedCache } from '../services/canvasRenderer' ;
65import { clearCompositorCache } from '../services/templateCompositor' ;
7-
8- // Storage keys for portrait images (separate from main store to handle size limits)
9- const PORTRAIT_STORAGE_KEY = 'money-generator-portrait' ;
10- const BG_REMOVED_STORAGE_KEY = 'money-generator-bg-removed' ;
11-
12- /**
13- * Save portrait to localStorage with fallback
14- * Clears old data before saving to prevent overflow
15- */
16- async function savePortraitToStorage ( imageDataUrl : string | null ) : Promise < void > {
17- try {
18- // Always clear old portrait first to prevent overflow
19- localStorage . removeItem ( PORTRAIT_STORAGE_KEY ) ;
20-
21- if ( ! imageDataUrl ) return ;
22-
23- // Resize for storage (1200px max, PNG for transparency)
24- const resized = await resizeImageForStorage ( imageDataUrl ) ;
25-
26- try {
27- localStorage . setItem ( PORTRAIT_STORAGE_KEY , resized ) ;
28- } catch ( e ) {
29- // Storage quota exceeded - silently fail, image will only be in memory
30- console . warn ( 'Could not persist portrait to localStorage:' , e ) ;
31- }
32- } catch ( e ) {
33- console . warn ( 'Error processing portrait for storage:' , e ) ;
34- }
35- }
36-
37- /**
38- * Save background-removed image to localStorage
39- */
40- async function saveBgRemovedToStorage ( imageDataUrl : string | null ) : Promise < void > {
41- try {
42- // Always clear old image first to prevent overflow
43- localStorage . removeItem ( BG_REMOVED_STORAGE_KEY ) ;
44-
45- if ( ! imageDataUrl ) return ;
46-
47- // Resize for storage (1200px max, PNG for transparency)
48- const resized = await resizeImageForStorage ( imageDataUrl ) ;
49-
50- try {
51- localStorage . setItem ( BG_REMOVED_STORAGE_KEY , resized ) ;
52- } catch ( e ) {
53- // Storage quota exceeded - silently fail, image will only be in memory
54- console . warn ( 'Could not persist bg-removed image to localStorage:' , e ) ;
55- }
56- } catch ( e ) {
57- console . warn ( 'Error processing bg-removed image for storage:' , e ) ;
58- }
59- }
60-
61- /**
62- * Load portrait from localStorage
63- */
64- function loadPortraitFromStorage ( ) : string | null {
65- try {
66- return localStorage . getItem ( PORTRAIT_STORAGE_KEY ) ;
67- } catch {
68- return null ;
69- }
70- }
71-
72- /**
73- * Load background-removed image from localStorage
74- */
75- function loadBgRemovedFromStorage ( ) : string | null {
76- try {
77- return localStorage . getItem ( BG_REMOVED_STORAGE_KEY ) ;
78- } catch {
79- return null ;
80- }
81- }
82-
83- /**
84- * Clear all portrait data from localStorage
85- */
86- function clearPortraitFromStorage ( ) : void {
87- try {
88- localStorage . removeItem ( PORTRAIT_STORAGE_KEY ) ;
89- localStorage . removeItem ( BG_REMOVED_STORAGE_KEY ) ;
90- } catch {
91- // Ignore errors
92- }
93- }
6+ import { indexedDBStorage } from './indexedDBStorage' ;
947
958// Detect browser language and return 'de' or 'en'
969function getBrowserLanguage ( ) : Language {
@@ -173,10 +86,6 @@ export const useBillStore = create<BillState & BillActions>()(
17386 } ) ) ,
17487
17588 setPortrait : ( original , enhanced = null ) => {
176- // NOTE: Do NOT save processed images to localStorage here!
177- // Only rawImage should be persisted (via setPortraitRawImage)
178- // portrait.original is computed on-the-fly from rawImage + effects
179-
18089 return set ( ( state ) => {
18190 // Keep zoom/pan if we already have a portrait (original OR rawImage)
18291 // This preserves settings during reload when original is null but rawImage exists
@@ -231,9 +140,6 @@ export const useBillStore = create<BillState & BillActions>()(
231140 } ) ) ,
232141
233142 setPortraitRawImage : ( rawImage ) => {
234- // Save rawImage to localStorage (this is the TRUE original, never processed)
235- savePortraitToStorage ( rawImage ) ;
236-
237143 return set ( ( state ) => ( {
238144 portrait : {
239145 ...state . portrait ,
@@ -243,11 +149,6 @@ export const useBillStore = create<BillState & BillActions>()(
243149 } ,
244150
245151 setPortraitBgRemoved : ( bgRemoved , bgRemovedImage ) => {
246- // Save bg-removed image to localStorage asynchronously
247- if ( bgRemovedImage !== undefined ) {
248- saveBgRemovedToStorage ( bgRemovedImage ) ;
249- }
250-
251152 return set ( ( state ) => ( {
252153 portrait : {
253154 ...state . portrait ,
@@ -314,12 +215,16 @@ export const useBillStore = create<BillState & BillActions>()(
314215 } ,
315216
316217 reset : ( ) => {
317- clearPortraitFromStorage ( ) ;
318218 return set ( initialState ) ;
319219 } ,
320220 } ) ,
321221 {
322222 name : 'money-generator-storage' ,
223+ // Use IndexedDB for storage - handles large images without localStorage limits
224+ storage : createJSONStorage ( ( ) => indexedDBStorage ) ,
225+ // Skip automatic hydration - consumer must call useBillStore.persist.rehydrate()
226+ // This prevents SSR/client mismatch issues with Next.js
227+ skipHydration : true ,
323228 // Migrate old storage - preserve user settings
324229 migrate : ( persistedState : unknown ) => {
325230 const state = persistedState as BillState ;
@@ -336,25 +241,21 @@ export const useBillStore = create<BillState & BillActions>()(
336241 }
337242 return state ;
338243 } ,
339- version : 2 , // Bump version to trigger migration
244+ version : 3 , // Bump version for IndexedDB migration
245+ // Store everything in IndexedDB - no size limits!
340246 partialize : ( state ) : BillState => ( {
341247 personalInfo : state . personalInfo ,
342- voucherConfig : {
343- ...state . voucherConfig ,
344- // Persist all voucher settings including templateHue
345- } ,
346- appLanguage : state . appLanguage , // Persist app language selection
347- // Only persist small settings, NOT image data (too large for localStorage)
248+ voucherConfig : state . voucherConfig ,
249+ appLanguage : state . appLanguage ,
348250 portrait : {
349- original : null , // Don't persist - too large
350- enhanced : null , // Don't persist - too large
251+ original : null , // Don't persist processed image - recompute from rawImage
252+ enhanced : null , // Don't persist - recompute if needed
351253 useEnhanced : state . portrait . useEnhanced ,
352254 zoom : state . portrait . zoom ,
353255 panX : state . portrait . panX ,
354256 panY : state . portrait . panY ,
355- rawImage : null , // Don't persist - too large
356- bgRemovedImage : null , // Don't persist - too large
357- // Persist background removal settings so UI shows correct controls after reload
257+ rawImage : state . portrait . rawImage , // Persist raw image in IndexedDB
258+ bgRemovedImage : state . portrait . bgRemovedImage , // Persist bg-removed in IndexedDB
358259 bgRemoved : state . portrait . bgRemoved ,
359260 bgOpacity : state . portrait . bgOpacity ,
360261 bgBlur : state . portrait . bgBlur ,
@@ -368,39 +269,12 @@ export const useBillStore = create<BillState & BillActions>()(
368269 )
369270) ;
370271
371- // Initialize portrait from localStorage on app start
372- // This runs once when the module is loaded
373- if ( typeof window !== 'undefined' ) {
374- const storedRawImage = loadPortraitFromStorage ( ) ;
375- const storedBgRemoved = loadBgRemovedFromStorage ( ) ;
376-
377- if ( storedRawImage ) {
378- // Use setTimeout to ensure store is fully initialized
379- setTimeout ( ( ) => {
380- const state = useBillStore . getState ( ) ;
381- // Only restore if no rawImage is currently set (avoid overwriting fresh uploads)
382- if ( ! state . portrait . rawImage ) {
383- // Check if we have bgRemoved state but missing image - reset if so
384- const hasBgRemovedState = state . portrait . bgRemoved ;
385- const hasBgRemovedImage = storedBgRemoved !== null ;
386-
387- useBillStore . setState ( {
388- portrait : {
389- ...state . portrait ,
390- // Set rawImage from storage (this is the TRUE original)
391- rawImage : storedRawImage ,
392- // DON'T set portrait.original here - it will be computed by PortraitUpload
393- // when it detects rawImage exists but original is null
394- original : null ,
395- // Restore bg-removed image if available
396- bgRemovedImage : storedBgRemoved ,
397- // Only keep bgRemoved state if we have the image
398- bgRemoved : hasBgRemovedState && hasBgRemovedImage ,
399- bgOpacity : hasBgRemovedState && hasBgRemovedImage ? state . portrait . bgOpacity : 0 ,
400- bgBlur : hasBgRemovedState && hasBgRemovedImage ? state . portrait . bgBlur : 0 ,
401- } ,
402- } ) ;
403- }
404- } , 0 ) ;
272+ /**
273+ * Initialize the bill store - must be called on client-side
274+ * This triggers hydration from IndexedDB
275+ */
276+ export function initializeBillStore ( ) : void {
277+ if ( typeof window !== 'undefined' ) {
278+ useBillStore . persist . rehydrate ( ) ;
405279 }
406280}
0 commit comments