@@ -17,14 +17,17 @@ import type { FeatureCollection } from "geojson";
1717// Types
1818// ---------------------------------------------------------------------------
1919
20+ export type LayoutMode = "hybrid" | "list" | "map" ;
21+ export type EntityTab = "utilities" | "grid-operators" | "power-plants" | "programs" | "transmission-lines" ;
2022export type ViewMode = "landing" | "list" | "detail" ;
21- export type ListView = "utilities" | "grid-operators" | "programs" ;
2223export type DetailView = "utility" | "iso" | "rto" | "ba" | "program" ;
23- export type EntityView = ListView | DetailView ;
24+ export type ListView = EntityTab ;
25+ export type EntityView = EntityTab | DetailView ;
2426
2527export interface ExplorerState {
28+ layout : LayoutMode ;
29+ tab : EntityTab ;
2630 mode : ViewMode ;
27- view : EntityView | null ;
2831 slug : string | null ;
2932 // List filters (persisted in URL)
3033 q : string ;
@@ -35,26 +38,35 @@ export interface ExplorerState {
3538 highlightGeoJSON : FeatureCollection | null ;
3639 hoveredSlug : string | null ;
3740 // Navigation history for back button
38- previousView : { view : EntityView | null ; slug : string | null } | null ;
41+ previousView : { tab : EntityTab ; slug : string | null } | null ;
3942}
4043
4144type ExplorerAction =
42- | { type : "NAVIGATE_LANDING" }
43- | { type : "NAVIGATE_LIST" ; view : ListView }
45+ | { type : "NAVIGATE_TAB" ; tab : EntityTab }
4446 | { type : "NAVIGATE_DETAIL" ; view : DetailView ; slug : string }
47+ | { type : "SET_LAYOUT" ; layout : LayoutMode }
4548 | { type : "SET_SEARCH" ; q : string }
4649 | { type : "SET_SEGMENT" ; segment : string }
4750 | { type : "SET_TYPE" ; typeFilter : string }
4851 | { type : "SET_JURISDICTIONS" ; jurisdictions : string [ ] }
4952 | { type : "SET_HIGHLIGHT" ; geoJSON : FeatureCollection | null }
5053 | { type : "SET_HOVERED_SLUG" ; slug : string | null }
51- | { type : "SYNC_FROM_URL" ; mode : ViewMode ; view : EntityView | null ; slug : string | null ; q : string ; segment : string ; typeFilter : string ; jurisdictions : string [ ] } ;
54+ | {
55+ type : "SYNC_FROM_URL" ;
56+ layout : LayoutMode ;
57+ tab : EntityTab ;
58+ slug : string | null ;
59+ q : string ;
60+ segment : string ;
61+ typeFilter : string ;
62+ jurisdictions : string [ ] ;
63+ } ;
5264
5365interface ExplorerContextValue {
5466 state : ExplorerState ;
55- navigateToLanding : ( ) => void ;
56- navigateToList : ( view : ListView ) => void ;
67+ navigateToTab : ( tab : EntityTab ) => void ;
5768 navigateToDetail : ( view : DetailView , slug : string ) => void ;
69+ setLayout : ( layout : LayoutMode ) => void ;
5870 setSearch : ( q : string ) => void ;
5971 setSegment : ( segment : string ) => void ;
6072 setTypeFilter : ( type : string ) => void ;
@@ -69,8 +81,9 @@ interface ExplorerContextValue {
6981// ---------------------------------------------------------------------------
7082
7183const initialState : ExplorerState = {
84+ layout : "hybrid" ,
85+ tab : "utilities" ,
7286 mode : "list" ,
73- view : "utilities" ,
7487 slug : null ,
7588 q : "" ,
7689 segment : "all" ,
@@ -83,11 +96,11 @@ const initialState: ExplorerState = {
8396
8497function reducer ( state : ExplorerState , action : ExplorerAction ) : ExplorerState {
8598 switch ( action . type ) {
86- case "NAVIGATE_LANDING " :
99+ case "NAVIGATE_TAB " :
87100 return {
88101 ...state ,
102+ tab : action . tab ,
89103 mode : "list" ,
90- view : "utilities" ,
91104 slug : null ,
92105 q : "" ,
93106 segment : "all" ,
@@ -98,27 +111,18 @@ function reducer(state: ExplorerState, action: ExplorerAction): ExplorerState {
98111 previousView : null ,
99112 } ;
100113
101- case "NAVIGATE_LIST" :
102- return {
103- ...state ,
104- mode : "list" ,
105- view : action . view ,
106- slug : null ,
107- highlightGeoJSON : null ,
108- hoveredSlug : null ,
109- previousView : { view : state . view , slug : state . slug } ,
110- } ;
111-
112114 case "NAVIGATE_DETAIL" :
113115 return {
114116 ...state ,
115117 mode : "detail" ,
116- view : action . view ,
117118 slug : action . slug ,
118119 hoveredSlug : null ,
119- previousView : { view : state . view , slug : state . slug } ,
120+ previousView : { tab : state . tab , slug : state . slug } ,
120121 } ;
121122
123+ case "SET_LAYOUT" :
124+ return { ...state , layout : action . layout } ;
125+
122126 case "SET_SEARCH" :
123127 return { ...state , q : action . q } ;
124128
@@ -140,13 +144,14 @@ function reducer(state: ExplorerState, action: ExplorerAction): ExplorerState {
140144 case "SYNC_FROM_URL" :
141145 return {
142146 ...state ,
143- mode : action . mode ,
144- view : action . view ,
147+ layout : action . layout ,
148+ tab : action . tab ,
145149 slug : action . slug ,
146150 q : action . q ,
147151 segment : action . segment ,
148152 type : action . typeFilter ,
149153 jurisdictions : action . jurisdictions ,
154+ mode : action . slug ? "detail" : "list" ,
150155 } ;
151156
152157 default :
@@ -160,25 +165,35 @@ function reducer(state: ExplorerState, action: ExplorerAction): ExplorerState {
160165
161166function stateToSearchParams ( state : ExplorerState ) : string {
162167 const params = new URLSearchParams ( ) ;
163- if ( state . view ) params . set ( "view" , state . view ) ;
168+ params . set ( "tab" , state . tab ) ;
169+ if ( state . layout !== "hybrid" ) params . set ( "layout" , state . layout ) ;
164170 if ( state . slug ) params . set ( "slug" , state . slug ) ;
165171 if ( state . q ) params . set ( "q" , state . q ) ;
166172 if ( state . segment && state . segment !== "all" ) params . set ( "segment" , state . segment ) ;
167173 if ( state . type && state . type !== "all" ) params . set ( "type" , state . type ) ;
168- if ( state . jurisdictions && state . jurisdictions . length > 0 ) params . set ( "jurisdictions" , state . jurisdictions . join ( "," ) ) ;
174+ if ( state . jurisdictions && state . jurisdictions . length > 0 )
175+ params . set ( "jurisdictions" , state . jurisdictions . join ( "," ) ) ;
169176 const str = params . toString ( ) ;
170177 return str ? `/explore?${ str } ` : "/explore" ;
171178}
172179
173- function parseViewMode ( view : string | null ) : { mode : ViewMode ; view : EntityView | null } {
174- if ( ! view ) return { mode : "list" , view : "utilities" } ;
175- if ( view === "utilities" || view === "grid-operators" || view === "programs" ) {
176- return { mode : "list" , view } ;
177- }
178- if ( view === "utility" || view === "iso" || view === "rto" || view === "ba" || view === "program" ) {
179- return { mode : "detail" , view } ;
180- }
181- return { mode : "list" , view : "utilities" } ;
180+ function parseTab ( value : string | null ) : EntityTab {
181+ const valid : EntityTab [ ] = [
182+ "utilities" ,
183+ "grid-operators" ,
184+ "power-plants" ,
185+ "programs" ,
186+ "transmission-lines" ,
187+ ] ;
188+ // backwards-compat: old "view" param values that were list views
189+ if ( value === "grid-operators" || value === "programs" ) return value ;
190+ if ( valid . includes ( value as EntityTab ) ) return value as EntityTab ;
191+ return "utilities" ;
192+ }
193+
194+ function parseLayout ( value : string | null ) : LayoutMode {
195+ if ( value === "list" || value === "map" || value === "hybrid" ) return value ;
196+ return "hybrid" ;
182197}
183198
184199// ---------------------------------------------------------------------------
@@ -205,20 +220,24 @@ export function ExplorerProvider({ children }: { children: ReactNode }) {
205220
206221 // Sync state FROM URL on mount and on popstate (browser back/forward)
207222 useEffect ( ( ) => {
208- const viewParam = searchParams . get ( "view" ) ;
223+ // Support old ?view= param for backwards compat
224+ const tabParam = searchParams . get ( "tab" ) ?? searchParams . get ( "view" ) ;
209225 const slugParam = searchParams . get ( "slug" ) ;
226+ const layoutParam = searchParams . get ( "layout" ) ;
210227 const qParam = searchParams . get ( "q" ) ?? "" ;
211228 const segmentParam = searchParams . get ( "segment" ) ?? "all" ;
212229 const typeParam = searchParams . get ( "type" ) ?? "all" ;
213230 const jurisdictionsParam = searchParams . get ( "jurisdictions" ) ;
214- const jurisdictionsFromUrl = jurisdictionsParam ? jurisdictionsParam . split ( "," ) . filter ( Boolean ) : [ ] ;
231+ const jurisdictionsFromUrl = jurisdictionsParam
232+ ? jurisdictionsParam . split ( "," ) . filter ( Boolean )
233+ : [ ] ;
215234
216- const { mode, view } = parseViewMode ( viewParam ) ;
235+ const tab = parseTab ( tabParam ) ;
236+ const layout = parseLayout ( layoutParam ) ;
217237
218- // Only sync if URL differs from current state
219238 if (
220- mode !== state . mode ||
221- view !== state . view ||
239+ layout !== state . layout ||
240+ tab !== state . tab ||
222241 slugParam !== state . slug ||
223242 qParam !== state . q ||
224243 segmentParam !== state . segment ||
@@ -228,8 +247,8 @@ export function ExplorerProvider({ children }: { children: ReactNode }) {
228247 isUrlSync . current = true ;
229248 dispatch ( {
230249 type : "SYNC_FROM_URL" ,
231- mode ,
232- view ,
250+ layout ,
251+ tab ,
233252 slug : slugParam ,
234253 q : qParam ,
235254 segment : segmentParam ,
@@ -249,18 +268,33 @@ export function ExplorerProvider({ children }: { children: ReactNode }) {
249268 const url = stateToSearchParams ( state ) ;
250269 router . push ( url , { scroll : false } ) ;
251270 // eslint-disable-next-line react-hooks/exhaustive-deps
252- } , [ state . mode , state . view , state . slug , state . q , state . segment , state . type , state . jurisdictions ] ) ;
271+ } , [ state . layout , state . tab , state . slug , state . q , state . segment , state . type , state . jurisdictions ] ) ;
253272
254- const navigateToLanding = useCallback ( ( ) => dispatch ( { type : "NAVIGATE_LANDING" } ) , [ ] ) ;
255- const navigateToList = useCallback ( ( view : ListView ) => dispatch ( { type : "NAVIGATE_LIST" , view } ) , [ ] ) ;
273+ const navigateToTab = useCallback (
274+ ( tab : EntityTab ) => dispatch ( { type : "NAVIGATE_TAB" , tab } ) ,
275+ [ ]
276+ ) ;
256277 const navigateToDetail = useCallback (
257278 ( view : DetailView , slug : string ) => dispatch ( { type : "NAVIGATE_DETAIL" , view, slug } ) ,
258279 [ ]
259280 ) ;
281+ const setLayout = useCallback (
282+ ( layout : LayoutMode ) => dispatch ( { type : "SET_LAYOUT" , layout } ) ,
283+ [ ]
284+ ) ;
260285 const setSearch = useCallback ( ( q : string ) => dispatch ( { type : "SET_SEARCH" , q } ) , [ ] ) ;
261- const setSegment = useCallback ( ( segment : string ) => dispatch ( { type : "SET_SEGMENT" , segment } ) , [ ] ) ;
262- const setTypeFilter = useCallback ( ( type : string ) => dispatch ( { type : "SET_TYPE" , typeFilter : type } ) , [ ] ) ;
263- const setJurisdictions = useCallback ( ( jurisdictions : string [ ] ) => dispatch ( { type : "SET_JURISDICTIONS" , jurisdictions } ) , [ ] ) ;
286+ const setSegment = useCallback (
287+ ( segment : string ) => dispatch ( { type : "SET_SEGMENT" , segment } ) ,
288+ [ ]
289+ ) ;
290+ const setTypeFilter = useCallback (
291+ ( type : string ) => dispatch ( { type : "SET_TYPE" , typeFilter : type } ) ,
292+ [ ]
293+ ) ;
294+ const setJurisdictions = useCallback (
295+ ( jurisdictions : string [ ] ) => dispatch ( { type : "SET_JURISDICTIONS" , jurisdictions } ) ,
296+ [ ]
297+ ) ;
264298 const setHighlight = useCallback (
265299 ( geoJSON : FeatureCollection | null ) => dispatch ( { type : "SET_HIGHLIGHT" , geoJSON } ) ,
266300 [ ]
@@ -272,26 +306,19 @@ export function ExplorerProvider({ children }: { children: ReactNode }) {
272306
273307 const goBack = useCallback ( ( ) => {
274308 const prev = state . previousView ;
275- if ( ! prev || ! prev . view ) {
276- dispatch ( { type : "NAVIGATE_LANDING" } ) ;
309+ if ( ! prev ) {
310+ dispatch ( { type : "NAVIGATE_TAB" , tab : state . tab } ) ;
277311 return ;
278312 }
279- // If previous was a list view, navigate back to list
280- if ( prev . view === "utilities" || prev . view === "grid-operators" || prev . view === "programs" ) {
281- dispatch ( { type : "NAVIGATE_LIST" , view : prev . view } ) ;
282- } else if ( prev . slug ) {
283- dispatch ( { type : "NAVIGATE_DETAIL" , view : prev . view , slug : prev . slug } ) ;
284- } else {
285- dispatch ( { type : "NAVIGATE_LANDING" } ) ;
286- }
287- } , [ state . previousView ] ) ;
313+ dispatch ( { type : "NAVIGATE_TAB" , tab : prev . tab } ) ;
314+ } , [ state . previousView , state . tab ] ) ;
288315
289316 const value = useMemo < ExplorerContextValue > (
290317 ( ) => ( {
291318 state,
292- navigateToLanding,
293- navigateToList,
319+ navigateToTab,
294320 navigateToDetail,
321+ setLayout,
295322 setSearch,
296323 setSegment,
297324 setTypeFilter,
@@ -300,7 +327,19 @@ export function ExplorerProvider({ children }: { children: ReactNode }) {
300327 setHoveredSlug,
301328 goBack,
302329 } ) ,
303- [ state , navigateToLanding , navigateToList , navigateToDetail , setSearch , setSegment , setTypeFilter , setJurisdictions , setHighlight , setHoveredSlug , goBack ]
330+ [
331+ state ,
332+ navigateToTab ,
333+ navigateToDetail ,
334+ setLayout ,
335+ setSearch ,
336+ setSegment ,
337+ setTypeFilter ,
338+ setJurisdictions ,
339+ setHighlight ,
340+ setHoveredSlug ,
341+ goBack ,
342+ ]
304343 ) ;
305344
306345 return < ExplorerCtx . Provider value = { value } > { children } </ ExplorerCtx . Provider > ;
0 commit comments