11import { useState } from 'react' ;
2+
3+ function getOperationErrorMessage ( err : unknown ) : string {
4+ const code = ( err as { code ?: string } ) ?. code ?? '' ;
5+ if ( code === 'unavailable' || code === 'network-request-failed' ) {
6+ return 'Unable to save — check your connection and try again.' ;
7+ }
8+ if ( code === 'permission-denied' ) {
9+ return 'Permission denied. Please sign out and sign back in.' ;
10+ }
11+ if ( err instanceof Error && err . message ) return err . message ;
12+ return 'Something went wrong. Please try again.' ;
13+ }
214import { Settings } from 'lucide-react' ;
315import {
416 MemberForm ,
@@ -51,6 +63,7 @@ function App() {
5163
5264 const [ view , setView ] = useState < View > ( 'home' ) ;
5365 const [ activeTab , setActiveTab ] = useState < TopLevelTab > ( 'family' ) ;
66+ const [ operationError , setOperationError ] = useState < string | null > ( null ) ;
5467 const [ selectedMember , setSelectedMember ] = useState < FamilyMember | null > ( null ) ;
5568 const [ selectedGearItem , setSelectedGearItem ] = useState < GearItem | null > ( null ) ;
5669 const [ gearOwnerId , setGearOwnerId ] = useState < string | null > ( null ) ;
@@ -70,25 +83,37 @@ function App() {
7083 data : Omit < FamilyMember , 'id' | 'userId' | 'createdAt' | 'updatedAt' >
7184 ) => {
7285 if ( ! user ) return ;
73- await addMember ( { ...data , userId : user . uid } ) ;
74- setView ( 'home' ) ;
86+ try {
87+ await addMember ( { ...data , userId : user . uid } ) ;
88+ setView ( 'home' ) ;
89+ } catch ( err ) {
90+ setOperationError ( getOperationErrorMessage ( err ) ) ;
91+ }
7592 } ;
7693
7794 const handleUpdateMember = async (
7895 data : Omit < FamilyMember , 'id' | 'userId' | 'createdAt' | 'updatedAt' >
7996 ) => {
8097 if ( selectedMember ) {
81- await updateMember ( selectedMember . id , data ) ;
82- setSelectedMember ( { ...selectedMember , ...data } ) ;
83- setView ( 'detail' ) ;
98+ try {
99+ await updateMember ( selectedMember . id , data ) ;
100+ setSelectedMember ( { ...selectedMember , ...data } ) ;
101+ setView ( 'detail' ) ;
102+ } catch ( err ) {
103+ setOperationError ( getOperationErrorMessage ( err ) ) ;
104+ }
84105 }
85106 } ;
86107
87108 const handleDeleteMember = async ( member : FamilyMember ) => {
88- await deleteMember ( member . id ) ;
89- if ( selectedMember ?. id === member . id ) {
90- setSelectedMember ( null ) ;
91- setView ( 'home' ) ;
109+ try {
110+ await deleteMember ( member . id ) ;
111+ if ( selectedMember ?. id === member . id ) {
112+ setSelectedMember ( null ) ;
113+ setView ( 'home' ) ;
114+ }
115+ } catch ( err ) {
116+ setOperationError ( getOperationErrorMessage ( err ) ) ;
92117 }
93118 } ;
94119
@@ -134,15 +159,19 @@ function App() {
134159 data : Omit < GearItem , 'id' | 'userId' | 'createdAt' | 'updatedAt' >
135160 ) => {
136161 if ( ! user ) return ;
137- if ( selectedGearItem ) {
138- await updateGearItem ( selectedGearItem . id , data ) ;
139- } else {
140- await addGearItem ( { ...data , userId : user . uid } ) ;
141- }
142- if ( gearDefaultSport && selectedMember ) {
143- setView ( 'sizing' ) ;
144- } else {
145- setView ( 'home' ) ;
162+ try {
163+ if ( selectedGearItem ) {
164+ await updateGearItem ( selectedGearItem . id , data ) ;
165+ } else {
166+ await addGearItem ( { ...data , userId : user . uid } ) ;
167+ }
168+ if ( gearDefaultSport && selectedMember ) {
169+ setView ( 'sizing' ) ;
170+ } else {
171+ setView ( 'home' ) ;
172+ }
173+ } catch ( err ) {
174+ setOperationError ( getOperationErrorMessage ( err ) ) ;
146175 }
147176 } ;
148177
@@ -181,100 +210,6 @@ function App() {
181210 ) ;
182211 }
183212
184- // ── Full-screen overlay views (no bottom nav) ────────────────────
185- if ( view === 'settings' ) {
186- return (
187- < div className = "app" >
188- < SettingsScreen
189- settings = { settings }
190- user = { user }
191- onUpdateSettings = { updateSettings }
192- onResetSettings = { resetSettings }
193- onSignOut = { signOut }
194- onSendPasswordReset = { sendPasswordReset }
195- onBack = { ( ) => setView ( 'home' ) }
196- />
197- </ div >
198- ) ;
199- }
200-
201- if ( view === 'add' || view === 'edit' ) {
202- return (
203- < div className = "app" >
204- < MemberForm
205- member = { view === 'edit' ? selectedMember ?? undefined : undefined }
206- onSubmit = { view === 'edit' ? handleUpdateMember : handleAddMember }
207- onCancel = { ( ) => setView ( selectedMember ? 'detail' : 'home' ) }
208- />
209- </ div >
210- ) ;
211- }
212-
213- if ( view === 'detail' && selectedMember ) {
214- const memberGear = gearItems . filter ( ( item ) => item . ownerId === selectedMember . id ) ;
215- return (
216- < div className = "app" >
217- < MemberDetail
218- member = { selectedMember }
219- gearItems = { memberGear }
220- settings = { settings }
221- onBack = { ( ) => { setSelectedMember ( null ) ; setView ( 'home' ) ; } }
222- onEdit = { ( ) => setView ( 'edit' ) }
223- onGetSizing = { ( ) => setView ( 'sizing' ) }
224- onOpenConverter = { ( ) => setView ( 'converter' ) }
225- onAddGear = { ( ) => handleAddGearFromSizing ( 'alpine' ) }
226- onEditGear = { handleEditGear }
227- />
228- </ div >
229- ) ;
230- }
231-
232- if ( view === 'sizing' && selectedMember ) {
233- const memberGear = gearItems . filter ( ( item ) => item . ownerId === selectedMember . id ) ;
234- return (
235- < div className = "app" >
236- < SportSizing
237- member = { selectedMember }
238- gearItems = { memberGear }
239- onBack = { ( ) => setView ( 'detail' ) }
240- onSkillLevelChange = { ( levels ) => updateSkillLevels ( selectedMember . id , levels ) }
241- onAddGear = { handleAddGearFromSizing }
242- onEditGear = { handleEditGear }
243- onDeleteGear = { handleDeleteGear }
244- />
245- </ div >
246- ) ;
247- }
248-
249- if ( view === 'converter' && selectedMember ) {
250- const footLength = Math . max (
251- selectedMember . measurements . footLengthLeft ,
252- selectedMember . measurements . footLengthRight
253- ) ;
254- return (
255- < div className = "app" >
256- < ShoeSizeConverter
257- footLengthCm = { footLength }
258- onClose = { ( ) => setView ( 'detail' ) }
259- />
260- </ div >
261- ) ;
262- }
263-
264- if ( ( view === 'add-gear' || view === 'edit-gear' ) && gearOwnerId ) {
265- return (
266- < div className = "app" >
267- < GearForm
268- item = { view === 'edit-gear' ? selectedGearItem ?? undefined : undefined }
269- ownerId = { gearOwnerId }
270- defaultSport = { gearDefaultSport }
271- onSubmit = { handleGearSubmit }
272- onCancel = { handleGearCancel }
273- />
274- </ div >
275- ) ;
276- }
277-
278213 // ── Main tabbed shell ────────────────────────────────────────────
279214 const renderTabContent = ( ) => {
280215 switch ( activeTab ) {
@@ -360,12 +295,115 @@ function App() {
360295 }
361296 } ;
362297
298+ const renderView = ( ) => {
299+ if ( view === 'settings' ) {
300+ return (
301+ < SettingsScreen
302+ settings = { settings }
303+ user = { user }
304+ onUpdateSettings = { updateSettings }
305+ onResetSettings = { resetSettings }
306+ onSignOut = { signOut }
307+ onSendPasswordReset = { sendPasswordReset }
308+ onBack = { ( ) => setView ( 'home' ) }
309+ />
310+ ) ;
311+ }
312+
313+ if ( view === 'add' || view === 'edit' ) {
314+ return (
315+ < MemberForm
316+ member = { view === 'edit' ? selectedMember ?? undefined : undefined }
317+ onSubmit = { view === 'edit' ? handleUpdateMember : handleAddMember }
318+ onCancel = { ( ) => setView ( selectedMember ? 'detail' : 'home' ) }
319+ />
320+ ) ;
321+ }
322+
323+ if ( view === 'detail' && selectedMember ) {
324+ const memberGear = gearItems . filter ( ( item ) => item . ownerId === selectedMember . id ) ;
325+ return (
326+ < MemberDetail
327+ member = { selectedMember }
328+ gearItems = { memberGear }
329+ settings = { settings }
330+ onBack = { ( ) => { setSelectedMember ( null ) ; setView ( 'home' ) ; } }
331+ onEdit = { ( ) => setView ( 'edit' ) }
332+ onGetSizing = { ( ) => setView ( 'sizing' ) }
333+ onOpenConverter = { ( ) => setView ( 'converter' ) }
334+ onAddGear = { ( ) => handleAddGearFromSizing ( 'alpine' ) }
335+ onEditGear = { handleEditGear }
336+ />
337+ ) ;
338+ }
339+
340+ if ( view === 'sizing' && selectedMember ) {
341+ const memberGear = gearItems . filter ( ( item ) => item . ownerId === selectedMember . id ) ;
342+ return (
343+ < SportSizing
344+ member = { selectedMember }
345+ gearItems = { memberGear }
346+ onBack = { ( ) => setView ( 'detail' ) }
347+ onSkillLevelChange = { ( levels ) => updateSkillLevels ( selectedMember . id , levels ) }
348+ onAddGear = { handleAddGearFromSizing }
349+ onEditGear = { handleEditGear }
350+ onDeleteGear = { handleDeleteGear }
351+ />
352+ ) ;
353+ }
354+
355+ if ( view === 'converter' && selectedMember ) {
356+ const footLength = Math . max (
357+ selectedMember . measurements . footLengthLeft ,
358+ selectedMember . measurements . footLengthRight
359+ ) ;
360+ return (
361+ < ShoeSizeConverter
362+ footLengthCm = { footLength }
363+ onClose = { ( ) => setView ( 'detail' ) }
364+ />
365+ ) ;
366+ }
367+
368+ if ( ( view === 'add-gear' || view === 'edit-gear' ) && gearOwnerId ) {
369+ return (
370+ < GearForm
371+ item = { view === 'edit-gear' ? selectedGearItem ?? undefined : undefined }
372+ ownerId = { gearOwnerId }
373+ defaultSport = { gearDefaultSport }
374+ onSubmit = { handleGearSubmit }
375+ onCancel = { handleGearCancel }
376+ />
377+ ) ;
378+ }
379+
380+ // Default: main tabbed shell
381+ return (
382+ < >
383+ < div className = "flex-1 flex flex-col min-h-0 overflow-hidden" >
384+ { renderTabContent ( ) }
385+ </ div >
386+ < BottomNav activeTab = { activeTab } onChange = { handleTabChange } />
387+ </ >
388+ ) ;
389+ } ;
390+
363391 return (
364392 < div className = "app flex flex-col" style = { { minHeight : '100dvh' } } >
365- < div className = "flex-1 flex flex-col min-h-0 overflow-hidden" >
366- { renderTabContent ( ) }
367- </ div >
368- < BottomNav activeTab = { activeTab } onChange = { handleTabChange } />
393+ { renderView ( ) }
394+ { operationError && (
395+ < div
396+ role = "alert"
397+ className = "fixed bottom-20 left-4 right-4 z-50 bg-red-600 text-white px-4 py-3 rounded-xl shadow-lg flex items-center justify-between"
398+ >
399+ < span className = "text-sm font-semibold" > { operationError } </ span >
400+ < button
401+ onClick = { ( ) => setOperationError ( null ) }
402+ aria-label = "Dismiss error"
403+ className = "ml-3 text-white hover:text-red-200 font-bold text-lg leading-none"
404+ > ✕</ button >
405+ </ div >
406+ ) }
369407 </ div >
370408 ) ;
371409}
0 commit comments