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
This commit is contained in:
pac7
2025-10-07 15:25:37 +00:00
parent 8c2ec57f9f
commit 6737431379
11 changed files with 210 additions and 47 deletions

View File

@@ -1,4 +1,4 @@
modules = ["nodejs-20", "web"] modules = ["nodejs-20", "web", "deno-2"]
[nix] [nix]
channel = "stable-25_05" channel = "stable-25_05"

View File

@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { useSearch, SearchResult } from '@/hooks/useSearch'; import { useSearch, SearchResult } from '@/hooks/useSearch';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useToast } from '@/components/ui/use-toast';
interface AutocompleteSearchProps { interface AutocompleteSearchProps {
onResultSelect?: (result: SearchResult) => void; onResultSelect?: (result: SearchResult) => void;
@@ -29,6 +30,7 @@ export function AutocompleteSearch({
variant = 'default' variant = 'default'
}: AutocompleteSearchProps) { }: AutocompleteSearchProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast();
const searchRef = useRef<HTMLDivElement>(null); const searchRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -117,23 +119,44 @@ export function AutocompleteSearch({
if (onResultSelect) { if (onResultSelect) {
onResultSelect(searchResult); onResultSelect(searchResult);
} else { } else {
// Default navigation // Default navigation with null/undefined safety checks
switch (searchResult.type) { switch (searchResult.type) {
case 'park': 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; break;
case 'ride': case 'ride':
const parkSlug = (searchResult.data as any).park?.slug; const parkSlug = (searchResult.data as any)?.park?.slug;
const rideSlug = searchResult.slug; const rideSlug = searchResult.slug;
const rideId = searchResult.id;
if (parkSlug && rideSlug) { if (parkSlug && rideSlug) {
navigate(`/parks/${parkSlug}/rides/${rideSlug}`); navigate(`/parks/${parkSlug}/rides/${rideSlug}`);
} else if (rideId) {
navigate(`/rides/${rideId}`);
} else { } 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; break;
case 'company': case 'company':
const companyType = (searchResult.data as any).company_type; const companyType = (searchResult.data as any)?.company_type;
const companySlug = searchResult.slug; const companySlug = searchResult.slug;
const companyId = searchResult.id;
if (companyType && companySlug) { if (companyType && companySlug) {
switch (companyType) { switch (companyType) {
case 'operator': case 'operator':
@@ -149,10 +172,26 @@ export function AutocompleteSearch({
navigate(`/designers/${companySlug}`); navigate(`/designers/${companySlug}`);
break; break;
default: 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 { } 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; break;
} }

View File

@@ -2,7 +2,7 @@ import * as React from "react";
const MOBILE_BREAKPOINT = 768; const MOBILE_BREAKPOINT = 768;
export function useIsMobile() { export function useIsMobile(): boolean | undefined {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined); const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
React.useEffect(() => { React.useEffect(() => {
@@ -17,5 +17,5 @@ export function useIsMobile() {
return () => mql.removeEventListener("change", onChange); return () => mql.removeEventListener("change", onChange);
}, []); }, []);
return !!isMobile; return isMobile;
} }

View File

@@ -1,4 +1,5 @@
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import type { Json } from '@/integrations/supabase/types';
import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
import { uploadPendingImages } from './imageUploadHelper'; import { uploadPendingImages } from './imageUploadHelper';
@@ -21,11 +22,16 @@ export async function submitCompanyCreation(
// Upload any pending local images first // Upload any pending local images first
let processedImages = data.images; let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) { if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded); try {
processedImages = { const uploadedImages = await uploadPendingImages(data.images.uploaded);
...data.images, processedImages = {
uploaded: uploadedImages ...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 // Create the main submission record
@@ -59,7 +65,7 @@ export async function submitCompanyCreation(
founded_year: data.founded_year, founded_year: data.founded_year,
headquarters_location: data.headquarters_location, headquarters_location: data.headquarters_location,
company_type: companyType, company_type: companyType,
images: processedImages as any images: processedImages as unknown as Json
}, },
status: 'pending', status: 'pending',
order_index: 0 order_index: 0
@@ -88,11 +94,16 @@ export async function submitCompanyUpdate(
// Upload any pending local images first // Upload any pending local images first
let processedImages = data.images; let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) { if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded); try {
processedImages = { const uploadedImages = await uploadPendingImages(data.images.uploaded);
...data.images, processedImages = {
uploaded: uploadedImages ...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 // Create the main submission record
@@ -127,7 +138,7 @@ export async function submitCompanyUpdate(
website_url: data.website_url, website_url: data.website_url,
founded_year: data.founded_year, founded_year: data.founded_year,
headquarters_location: data.headquarters_location, headquarters_location: data.headquarters_location,
images: processedImages as any images: processedImages as unknown as Json
}, },
original_data: JSON.parse(JSON.stringify(existingCompany)), original_data: JSON.parse(JSON.stringify(existingCompany)),
status: 'pending', status: 'pending',

View File

@@ -1,4 +1,5 @@
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import type { Json } from '@/integrations/supabase/types';
import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader'; import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
import { uploadPendingImages } from './imageUploadHelper'; import { uploadPendingImages } from './imageUploadHelper';
@@ -142,11 +143,16 @@ export async function submitParkCreation(
// Upload any pending local images first // Upload any pending local images first
let processedImages = data.images; let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) { if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded); try {
processedImages = { const uploadedImages = await uploadPendingImages(data.images.uploaded);
...data.images, processedImages = {
uploaded: uploadedImages ...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 // Create the main submission record
@@ -173,7 +179,7 @@ export async function submitParkCreation(
item_type: 'park', item_type: 'park',
item_data: { item_data: {
...data, ...data,
images: processedImages as any images: processedImages as unknown as Json
}, },
status: 'pending', status: 'pending',
order_index: 0 order_index: 0
@@ -222,11 +228,16 @@ export async function submitParkUpdate(
// Upload any pending local images first // Upload any pending local images first
let processedImages = data.images; let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) { if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded); try {
processedImages = { const uploadedImages = await uploadPendingImages(data.images.uploaded);
...data.images, processedImages = {
uploaded: uploadedImages ...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 // Create the main submission record
@@ -293,11 +304,16 @@ export async function submitRideCreation(
// Upload any pending local images first // Upload any pending local images first
let processedImages = data.images; let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) { if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded); try {
processedImages = { const uploadedImages = await uploadPendingImages(data.images.uploaded);
...data.images, processedImages = {
uploaded: uploadedImages ...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 // Create the main submission record
@@ -324,7 +340,7 @@ export async function submitRideCreation(
item_type: 'ride', item_type: 'ride',
item_data: { item_data: {
...data, ...data,
images: processedImages as any images: processedImages as unknown as Json
}, },
status: 'pending', status: 'pending',
order_index: 0 order_index: 0
@@ -373,11 +389,16 @@ export async function submitRideUpdate(
// Upload any pending local images first // Upload any pending local images first
let processedImages = data.images; let processedImages = data.images;
if (data.images?.uploaded && data.images.uploaded.length > 0) { if (data.images?.uploaded && data.images.uploaded.length > 0) {
const uploadedImages = await uploadPendingImages(data.images.uploaded); try {
processedImages = { const uploadedImages = await uploadPendingImages(data.images.uploaded);
...data.images, processedImages = {
uploaded: uploadedImages ...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 // Create the main submission record

View File

@@ -107,7 +107,7 @@ Deno.serve(async (req) => {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
success: false, success: false,
error: error.message, error: error instanceof Error ? error.message : 'An unknown error occurred',
}), }),
{ {
headers: { ...corsHeaders, 'Content-Type': 'application/json' }, headers: { ...corsHeaders, 'Content-Type': 'application/json' },

36
supabase/functions/deno.d.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
/// <reference lib="deno.ns" />
declare module 'https://deno.land/std@*/http/server.ts' {
export function serve(handler: (req: Request) => Response | Promise<Response>, options?: { port?: number }): void;
}
declare module 'https://deno.land/std@0.168.0/http/server.ts' {
export function serve(handler: (req: Request) => Response | Promise<Response>, options?: { port?: number }): void;
}
declare module 'https://deno.land/std@0.190.0/http/server.ts' {
export function serve(handler: (req: Request) => Response | Promise<Response>, 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<Response>,
options?: { port?: number; hostname?: string }
): void;
}

View File

@@ -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
}
}

View File

@@ -230,7 +230,7 @@ serve(async (req) => {
itemId: item.id, itemId: item.id,
itemType: item.item_type, itemType: item.item_type,
success: false, success: false,
error: error.message error: error instanceof Error ? error.message : 'Unknown error'
}); });
} }
} }
@@ -261,7 +261,7 @@ serve(async (req) => {
} catch (error) { } catch (error) {
console.error('Error in process-selective-approval:', error); console.error('Error in process-selective-approval:', error);
return new Response( 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' } } { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
); );
} }

View File

@@ -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"
]
}

View File

@@ -1,6 +1,7 @@
{ {
"files": [], "files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"exclude": ["node_modules", "supabase"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {