1- import {
2- type ReactNode ,
3- useState ,
4- useMemo ,
5- useRef ,
6- useEffect ,
7- useCallback ,
8- startTransition ,
9- } from "react" ;
1+ import { type ReactNode , useState , useMemo , useRef , useEffect , useCallback } from "react" ;
2+ import { useParams , useNavigate } from "react-router" ;
103import type { Action } from "@kreozalabs/kei-core" ;
114import { useSettings } from "@/providers/SettingsContext" ;
125import { useDb } from "@/providers/DbContext" ;
136import { useCurrentDay } from "@/hooks/useCurrentDay" ;
14- import { getTodayString , STORAGE_KEYS } from "@kreozalabs/kei-core" ;
7+ import { STORAGE_KEYS } from "@kreozalabs/kei-core" ;
158
169import type { ViewMode } from "../types" ;
1710import { useDashboardQueries } from "../hooks/useDashboardQueries" ;
@@ -21,51 +14,156 @@ import { DashboardContext, type DashboardContextValue } from "./DashboardContext
2114export function DashboardProvider ( { children } : { children : ReactNode } ) {
2215 const { settings } = useSettings ( ) ;
2316 const { isDbReady, dbError } = useDb ( ) ;
17+ const params = useParams ( ) ;
18+ const navigate = useNavigate ( ) ;
2419
25- const [ viewMode , setViewModeState ] = useState < ViewMode > ( "day" ) ;
2620 const todayStr = useCurrentDay ( ) ;
27- const [ selectedDate , setSelectedDateState ] = useState ( getTodayString ) ;
28- const [ startDateStr , setStartDateStr ] = useState ( getTodayString ) ;
21+
22+ // Parse splat parameters
23+ const splatParts = useMemo ( ( ) => {
24+ const splat = params [ "*" ] || "" ;
25+ return splat . split ( "/" ) . filter ( Boolean ) ;
26+ } , [ params ] ) ;
27+
28+ // 1. Derive viewMode
29+ const viewMode = useMemo ( ( ) => {
30+ const urlView = splatParts [ 0 ] as ViewMode | undefined ;
31+ const validViews : ViewMode [ ] = [ "day" , "week" , "month" , "year" , "agenda" , "inbox" , "lists" ] ;
32+ if ( urlView && validViews . includes ( urlView ) ) {
33+ return urlView ;
34+ }
35+
36+ if ( typeof window !== "undefined" ) {
37+ const stored = localStorage . getItem ( "kei_dashboard_view_mode" ) as ViewMode ;
38+ if ( stored && validViews . includes ( stored ) ) {
39+ return stored ;
40+ }
41+ }
42+ return "day" ;
43+ } , [ splatParts ] ) ;
44+
45+ // 2. Derive selectedDate
46+ const selectedDate = useMemo ( ( ) => {
47+ const year = splatParts [ 1 ] ;
48+ const month = splatParts [ 2 ] ;
49+ const day = splatParts [ 3 ] ;
50+
51+ if ( year && month && day ) {
52+ const y = year ;
53+ const m = month . padStart ( 2 , "0" ) ;
54+ const d = day . padStart ( 2 , "0" ) ;
55+ const dateStr = `${ y } -${ m } -${ d } ` ;
56+ if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / . test ( dateStr ) && ! isNaN ( Date . parse ( dateStr ) ) ) {
57+ return dateStr ;
58+ }
59+ }
60+
61+ if ( settings . remember_layout_on_refresh && typeof window !== "undefined" ) {
62+ const storedDate = localStorage . getItem ( STORAGE_KEYS . LOCAL . SELECTED_DATE ) ;
63+ if ( storedDate && / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / . test ( storedDate ) && ! isNaN ( Date . parse ( storedDate ) ) ) {
64+ return storedDate ;
65+ }
66+ }
67+
68+ return todayStr ;
69+ } , [ splatParts , settings . remember_layout_on_refresh , todayStr ] ) ;
70+
71+ const [ startDateStr , setStartDateStr ] = useState ( todayStr ) ;
2972 const [ endDateStr , setEndDateStr ] = useState ( ( ) => {
30- const d = new Date ( getTodayString ( ) ) ;
73+ const d = new Date ( todayStr ) ;
3174 d . setDate ( d . getDate ( ) + 3 ) ;
3275 return d . toISOString ( ) . split ( "T" ) [ 0 ] ;
3376 } ) ;
3477
35- // Restore client-side state on mount to prevent SSR hydration mismatch warnings
78+ // Sync query bounds when selectedDate changes
3679 useEffect ( ( ) => {
37- if ( typeof window !== "undefined" ) {
38- const storedViewMode = localStorage . getItem ( "kei_dashboard_view_mode" ) as ViewMode ;
39- if ( storedViewMode ) {
40- setViewModeState ( storedViewMode ) ;
41- }
80+ const isChronological = [ "day" , "week" , "month" , "year" , "agenda" ] . includes ( viewMode ) ;
81+ if ( ! isChronological ) {
82+ setStartDateStr ( selectedDate ) ;
83+ const d = new Date ( selectedDate ) ;
84+ d . setDate ( d . getDate ( ) + 3 ) ;
85+ setEndDateStr ( d . toISOString ( ) . split ( "T" ) [ 0 ] ) ;
86+ }
87+ } , [ selectedDate , viewMode ] ) ;
4288
43- if ( settings . remember_layout_on_refresh ) {
44- const storedDate = localStorage . getItem ( STORAGE_KEYS . LOCAL . SELECTED_DATE ) ;
45- if ( storedDate ) {
46- setSelectedDateState ( storedDate ) ;
47- }
89+ // Redirection / URL sync effect:
90+ // Ensure the browser URL matches the active viewMode and selectedDate.
91+ useEffect ( ( ) => {
92+ const isChronological = [ "day" , "week" , "month" , "year" , "agenda" ] . includes ( viewMode ) ;
93+
94+ const urlView = splatParts [ 0 ] ;
95+ const urlYear = splatParts [ 1 ] ;
96+ const urlMonth = splatParts [ 2 ] ;
97+ const urlDay = splatParts [ 3 ] ;
98+
99+ if ( isChronological ) {
100+ const [ y , m , d ] = selectedDate . split ( "-" ) ;
101+ const formattedYear = y ;
102+ const formattedMonth = String ( Number ( m ) ) ;
103+ const formattedDay = String ( Number ( d ) ) ;
104+
105+ if (
106+ urlView !== viewMode ||
107+ urlYear !== formattedYear ||
108+ urlMonth !== formattedMonth ||
109+ urlDay !== formattedDay
110+ ) {
111+ navigate ( `/app/calendar/${ viewMode } /${ formattedYear } /${ formattedMonth } /${ formattedDay } ` , {
112+ replace : true ,
113+ } ) ;
114+ }
115+ } else {
116+ // Structural views (inbox, lists)
117+ if ( urlView !== viewMode || urlYear !== undefined ) {
118+ navigate ( `/app/calendar/${ viewMode } ` , { replace : true } ) ;
48119 }
49120 }
50- } , [ settings . remember_layout_on_refresh ] ) ;
121+ } , [ viewMode , selectedDate , splatParts , navigate ] ) ;
51122
52- const setViewMode = useCallback ( ( val : ViewMode ) => {
53- startTransition ( ( ) => {
54- setViewModeState ( val ) ;
55- } ) ;
56- if ( typeof window !== "undefined" ) {
57- window . localStorage . setItem ( "kei_dashboard_view_mode" , val ) ;
58- }
59- } , [ ] ) ;
123+ const setViewMode = useCallback (
124+ ( val : ViewMode ) => {
125+ if ( typeof window !== "undefined" ) {
126+ window . localStorage . setItem ( "kei_dashboard_view_mode" , val ) ;
127+ }
128+
129+ const [ y , m , d ] = selectedDate . split ( "-" ) ;
130+ const formattedYear = y ;
131+ const formattedMonth = String ( Number ( m ) ) ;
132+ const formattedDay = String ( Number ( d ) ) ;
133+
134+ const isChronological = [ "day" , "week" , "month" , "year" , "agenda" ] . includes ( val ) ;
135+ if ( isChronological ) {
136+ navigate ( `/app/calendar/${ val } /${ formattedYear } /${ formattedMonth } /${ formattedDay } ` , {
137+ replace : true ,
138+ } ) ;
139+ } else {
140+ navigate ( `/app/calendar/${ val } ` , { replace : true } ) ;
141+ }
142+ } ,
143+ [ selectedDate , navigate ]
144+ ) ;
60145
61146 const setSelectedDate = useCallback (
62147 ( date : string ) => {
63- setSelectedDateState ( date ) ;
64- if ( settings . remember_layout_on_refresh ) {
148+ if ( settings . remember_layout_on_refresh && typeof window !== "undefined" ) {
65149 localStorage . setItem ( STORAGE_KEYS . LOCAL . SELECTED_DATE , date ) ;
66150 }
151+
152+ const [ y , m , d ] = date . split ( "-" ) ;
153+ const formattedYear = y ;
154+ const formattedMonth = String ( Number ( m ) ) ;
155+ const formattedDay = String ( Number ( d ) ) ;
156+
157+ const urlYear = splatParts [ 1 ] ;
158+ const urlMonth = splatParts [ 2 ] ;
159+ const urlDay = splatParts [ 3 ] ;
160+
161+ if ( urlYear !== formattedYear || urlMonth !== formattedMonth || urlDay !== formattedDay ) {
162+ const dest = `/app/calendar/${ viewMode } /${ formattedYear } /${ formattedMonth } /${ formattedDay } ` ;
163+ navigate ( dest , { replace : true } ) ;
164+ }
67165 } ,
68- [ settings . remember_layout_on_refresh ]
166+ [ viewMode , splatParts , settings . remember_layout_on_refresh , navigate ]
69167 ) ;
70168
71169 const queries = useDashboardQueries ( {
0 commit comments