From 673743137940d5467f13e0ccb3879186b5223851 Mon Sep 17 00:00:00 2001 From: pac7 <47831526-pac7@users.noreply.replit.com> Date: Tue, 7 Oct 2025 15:25:37 +0000 Subject: [PATCH] Improve error handling and navigation safety across the application Add robust error handling for image uploads, improve navigation logic in AutocompleteSearch with toast notifications for missing identifiers, refine useIsMobile hook return type, and update Supabase function error reporting to handle non-Error types. Replit-Commit-Author: Agent Replit-Commit-Session-Id: a759d451-40bf-440d-96f5-a19ad6af18a8 Replit-Commit-Checkpoint-Type: intermediate_checkpoint --- .replit | 2 +- src/components/search/AutocompleteSearch.tsx | 53 +++++++++++++-- src/hooks/use-mobile.tsx | 4 +- src/lib/companyHelpers.ts | 35 ++++++---- src/lib/entitySubmissionHelpers.ts | 65 ++++++++++++------- .../functions/cancel-email-change/index.ts | 2 +- supabase/functions/deno.d.ts | 36 ++++++++++ supabase/functions/deno.json | 25 +++++++ .../process-selective-approval/index.ts | 4 +- supabase/functions/tsconfig.json | 30 +++++++++ tsconfig.json | 1 + 11 files changed, 210 insertions(+), 47 deletions(-) create mode 100644 supabase/functions/deno.d.ts create mode 100644 supabase/functions/deno.json create mode 100644 supabase/functions/tsconfig.json diff --git a/.replit b/.replit index aae4bbec..fc81a45d 100644 --- a/.replit +++ b/.replit @@ -1,4 +1,4 @@ -modules = ["nodejs-20", "web"] +modules = ["nodejs-20", "web", "deno-2"] [nix] channel = "stable-25_05" diff --git a/src/components/search/AutocompleteSearch.tsx b/src/components/search/AutocompleteSearch.tsx index 24160beb..0598b288 100644 --- a/src/components/search/AutocompleteSearch.tsx +++ b/src/components/search/AutocompleteSearch.tsx @@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge'; import { Separator } from '@/components/ui/separator'; import { useSearch, SearchResult } from '@/hooks/useSearch'; import { useNavigate } from 'react-router-dom'; +import { useToast } from '@/components/ui/use-toast'; interface AutocompleteSearchProps { onResultSelect?: (result: SearchResult) => void; @@ -29,6 +30,7 @@ export function AutocompleteSearch({ variant = 'default' }: AutocompleteSearchProps) { const navigate = useNavigate(); + const { toast } = useToast(); const searchRef = useRef(null); const inputRef = useRef(null); const [isOpen, setIsOpen] = useState(false); @@ -117,23 +119,44 @@ export function AutocompleteSearch({ if (onResultSelect) { onResultSelect(searchResult); } else { - // Default navigation + // Default navigation with null/undefined safety checks switch (searchResult.type) { case 'park': - navigate(`/parks/${searchResult.slug || searchResult.id}`); + const parkIdentifier = searchResult.slug || searchResult.id; + if (parkIdentifier) { + navigate(`/parks/${parkIdentifier}`); + } else { + toast({ + title: "Navigation Error", + description: "Unable to navigate to this park. Missing park identifier.", + variant: "destructive", + }); + navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); + } break; case 'ride': - const parkSlug = (searchResult.data as any).park?.slug; + const parkSlug = (searchResult.data as any)?.park?.slug; const rideSlug = searchResult.slug; + const rideId = searchResult.id; + if (parkSlug && rideSlug) { navigate(`/parks/${parkSlug}/rides/${rideSlug}`); + } else if (rideId) { + navigate(`/rides/${rideId}`); } else { - navigate(`/rides/${searchResult.id}`); + toast({ + title: "Navigation Error", + description: "Unable to navigate to this ride. Missing ride identifier.", + variant: "destructive", + }); + navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); } break; case 'company': - const companyType = (searchResult.data as any).company_type; + const companyType = (searchResult.data as any)?.company_type; const companySlug = searchResult.slug; + const companyId = searchResult.id; + if (companyType && companySlug) { switch (companyType) { case 'operator': @@ -149,10 +172,26 @@ export function AutocompleteSearch({ navigate(`/designers/${companySlug}`); break; default: - navigate(`/companies/${searchResult.id}`); + if (companyId) { + navigate(`/companies/${companyId}`); + } else { + toast({ + title: "Navigation Error", + description: "Unable to navigate to this company. Missing company identifier.", + variant: "destructive", + }); + navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); + } } + } else if (companyId) { + navigate(`/companies/${companyId}`); } else { - navigate(`/companies/${searchResult.id}`); + toast({ + title: "Navigation Error", + description: "Unable to navigate to this company. Missing company identifier.", + variant: "destructive", + }); + navigate(`/search?q=${encodeURIComponent(searchResult.title)}`); } break; } diff --git a/src/hooks/use-mobile.tsx b/src/hooks/use-mobile.tsx index d71598a5..99548db2 100644 --- a/src/hooks/use-mobile.tsx +++ b/src/hooks/use-mobile.tsx @@ -2,7 +2,7 @@ import * as React from "react"; const MOBILE_BREAKPOINT = 768; -export function useIsMobile() { +export function useIsMobile(): boolean | undefined { const [isMobile, setIsMobile] = React.useState(undefined); React.useEffect(() => { @@ -17,5 +17,5 @@ export function useIsMobile() { return () => mql.removeEventListener("change", onChange); }, []); - return !!isMobile; + return isMobile; } diff --git a/src/lib/companyHelpers.ts b/src/lib/companyHelpers.ts index 78f90665..e6b9bb7e 100644 --- a/src/lib/companyHelpers.ts +++ b/src/lib/companyHelpers.ts @@ -1,4 +1,5 @@ import { supabase } from '@/integrations/supabase/client'; +import type { Json } from '@/integrations/supabase/types'; import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { uploadPendingImages } from './imageUploadHelper'; @@ -21,11 +22,16 @@ export async function submitCompanyCreation( // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(data.images.uploaded); - processedImages = { - ...data.images, - uploaded: uploadedImages - }; + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error(`Failed to upload images for ${companyType} creation:`, error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } } // Create the main submission record @@ -59,7 +65,7 @@ export async function submitCompanyCreation( founded_year: data.founded_year, headquarters_location: data.headquarters_location, company_type: companyType, - images: processedImages as any + images: processedImages as unknown as Json }, status: 'pending', order_index: 0 @@ -88,11 +94,16 @@ export async function submitCompanyUpdate( // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(data.images.uploaded); - processedImages = { - ...data.images, - uploaded: uploadedImages - }; + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error(`Failed to upload images for ${existingCompany.company_type} update:`, error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } } // Create the main submission record @@ -127,7 +138,7 @@ export async function submitCompanyUpdate( website_url: data.website_url, founded_year: data.founded_year, headquarters_location: data.headquarters_location, - images: processedImages as any + images: processedImages as unknown as Json }, original_data: JSON.parse(JSON.stringify(existingCompany)), status: 'pending', diff --git a/src/lib/entitySubmissionHelpers.ts b/src/lib/entitySubmissionHelpers.ts index 5fe6385d..c5762249 100644 --- a/src/lib/entitySubmissionHelpers.ts +++ b/src/lib/entitySubmissionHelpers.ts @@ -1,4 +1,5 @@ import { supabase } from '@/integrations/supabase/client'; +import type { Json } from '@/integrations/supabase/types'; import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { uploadPendingImages } from './imageUploadHelper'; @@ -142,11 +143,16 @@ export async function submitParkCreation( // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(data.images.uploaded); - processedImages = { - ...data.images, - uploaded: uploadedImages - }; + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error('Failed to upload images for park creation:', error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } } // Create the main submission record @@ -173,7 +179,7 @@ export async function submitParkCreation( item_type: 'park', item_data: { ...data, - images: processedImages as any + images: processedImages as unknown as Json }, status: 'pending', order_index: 0 @@ -222,11 +228,16 @@ export async function submitParkUpdate( // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(data.images.uploaded); - processedImages = { - ...data.images, - uploaded: uploadedImages - }; + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error('Failed to upload images for park update:', error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } } // Create the main submission record @@ -293,11 +304,16 @@ export async function submitRideCreation( // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(data.images.uploaded); - processedImages = { - ...data.images, - uploaded: uploadedImages - }; + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error('Failed to upload images for ride creation:', error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } } // Create the main submission record @@ -324,7 +340,7 @@ export async function submitRideCreation( item_type: 'ride', item_data: { ...data, - images: processedImages as any + images: processedImages as unknown as Json }, status: 'pending', order_index: 0 @@ -373,11 +389,16 @@ export async function submitRideUpdate( // Upload any pending local images first let processedImages = data.images; if (data.images?.uploaded && data.images.uploaded.length > 0) { - const uploadedImages = await uploadPendingImages(data.images.uploaded); - processedImages = { - ...data.images, - uploaded: uploadedImages - }; + try { + const uploadedImages = await uploadPendingImages(data.images.uploaded); + processedImages = { + ...data.images, + uploaded: uploadedImages + }; + } catch (error) { + console.error('Failed to upload images for ride update:', error); + throw new Error('Failed to upload images. Please check your connection and try again.'); + } } // Create the main submission record diff --git a/supabase/functions/cancel-email-change/index.ts b/supabase/functions/cancel-email-change/index.ts index 7750befc..5b731b23 100644 --- a/supabase/functions/cancel-email-change/index.ts +++ b/supabase/functions/cancel-email-change/index.ts @@ -107,7 +107,7 @@ Deno.serve(async (req) => { return new Response( JSON.stringify({ success: false, - error: error.message, + error: error instanceof Error ? error.message : 'An unknown error occurred', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, diff --git a/supabase/functions/deno.d.ts b/supabase/functions/deno.d.ts new file mode 100644 index 00000000..cc7cd3cd --- /dev/null +++ b/supabase/functions/deno.d.ts @@ -0,0 +1,36 @@ +/// + +declare module 'https://deno.land/std@*/http/server.ts' { + export function serve(handler: (req: Request) => Response | Promise, options?: { port?: number }): void; +} + +declare module 'https://deno.land/std@0.168.0/http/server.ts' { + export function serve(handler: (req: Request) => Response | Promise, options?: { port?: number }): void; +} + +declare module 'https://deno.land/std@0.190.0/http/server.ts' { + export function serve(handler: (req: Request) => Response | Promise, options?: { port?: number }): void; +} + +declare module 'https://esm.sh/@supabase/supabase-js@2' { + export * from '@supabase/supabase-js'; +} + +declare module 'https://esm.sh/@supabase/supabase-js@2.57.4' { + export * from '@supabase/supabase-js'; +} + +declare module 'npm:@novu/node@2.0.2' { + export * from '@novu/node'; +} + +declare namespace Deno { + export namespace env { + export function get(key: string): string | undefined; + } + + export function serve( + handler: (req: Request) => Response | Promise, + options?: { port?: number; hostname?: string } + ): void; +} diff --git a/supabase/functions/deno.json b/supabase/functions/deno.json new file mode 100644 index 00000000..ab4bf8e6 --- /dev/null +++ b/supabase/functions/deno.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["deno.window"], + "strict": true, + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "imports": { + "@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.57.4", + "std/": "https://deno.land/std@0.190.0/", + "@novu/node": "npm:@novu/node@2.0.2" + }, + "lint": { + "rules": { + "tags": ["recommended"] + } + }, + "fmt": { + "indentWidth": 2, + "lineWidth": 100, + "semiColons": true, + "singleQuote": false + } +} diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index a8d330c1..b24ce2e2 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -230,7 +230,7 @@ serve(async (req) => { itemId: item.id, itemType: item.item_type, success: false, - error: error.message + error: error instanceof Error ? error.message : 'Unknown error' }); } } @@ -261,7 +261,7 @@ serve(async (req) => { } catch (error) { console.error('Error in process-selective-approval:', error); return new Response( - JSON.stringify({ error: error.message }), + JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error' }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ); } diff --git a/supabase/functions/tsconfig.json b/supabase/functions/tsconfig.json new file mode 100644 index 00000000..5a29e14c --- /dev/null +++ b/supabase/functions/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "types": ["./deno.d.ts"], + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "baseUrl": ".", + "paths": { + "https://deno.land/*": ["*"], + "https://esm.sh/*": ["*"], + "npm:*": ["*"] + } + }, + "include": [ + "./**/*.ts", + "./deno.d.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 25187730..da662f36 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], + "exclude": ["node_modules", "supabase"], "compilerOptions": { "baseUrl": ".", "paths": {