Improve form validation and image handling for entities

Refactor validation logic for 'founded_year' in multiple form components, enhance image cleanup in `EntityMultiImageUploader`, update `useEntityVersions` to prevent race conditions, improve error handling for recent searches in `useSearch`, refine rate limiting logic in `detect-location` Supabase function, and update CORS configuration for `upload-image` Supabase function.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: b9af4867-23a7-43cc-baeb-4a97f66b4150
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-10-08 18:58:43 +00:00
parent a85c0fcd11
commit 4bdbdac7c3
9 changed files with 112 additions and 52 deletions

View File

@@ -22,8 +22,12 @@ const designerSchema = z.object({
website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.string()
.optional()
.transform(val => val === '' || val === undefined ? undefined : Number(val))
.refine(val => val === undefined || (!isNaN(val) && val >= 1800 && val <= new Date().getFullYear()), {
.transform(val => {
if (!val || val.trim() === '') return undefined;
const num = Number(val);
return isNaN(num) ? undefined : num;
})
.refine(val => val === undefined || (typeof val === 'number' && val >= 1800 && val <= new Date().getFullYear()), {
message: "Founded year must be between 1800 and current year"
}),
headquarters_location: z.string().optional(),
@@ -70,10 +74,10 @@ export function DesignerForm({ onSubmit, onCancel, initialData }: DesignerFormPr
description: initialData?.description || '',
person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined,
founded_year: initialData?.founded_year ? String(initialData.founded_year) : '',
headquarters_location: initialData?.headquarters_location || '',
images: initialData?.images || { uploaded: [] }
}
} as Partial<DesignerFormData>
});

View File

@@ -22,8 +22,12 @@ const manufacturerSchema = z.object({
website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.string()
.optional()
.transform(val => val === '' || val === undefined ? undefined : Number(val))
.refine(val => val === undefined || (!isNaN(val) && val >= 1800 && val <= new Date().getFullYear()), {
.transform(val => {
if (!val || val.trim() === '') return undefined;
const num = Number(val);
return isNaN(num) ? undefined : num;
})
.refine(val => val === undefined || (typeof val === 'number' && val >= 1800 && val <= new Date().getFullYear()), {
message: "Founded year must be between 1800 and current year"
}),
headquarters_location: z.string().optional(),
@@ -70,10 +74,10 @@ export function ManufacturerForm({ onSubmit, onCancel, initialData }: Manufactur
description: initialData?.description || '',
person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined,
founded_year: initialData?.founded_year ? String(initialData.founded_year) : '',
headquarters_location: initialData?.headquarters_location || '',
images: initialData?.images || { uploaded: [] }
}
} as Partial<ManufacturerFormData>
});

View File

@@ -22,8 +22,12 @@ const operatorSchema = z.object({
website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.string()
.optional()
.transform(val => val === '' || val === undefined ? undefined : Number(val))
.refine(val => val === undefined || (!isNaN(val) && val >= 1800 && val <= new Date().getFullYear()), {
.transform(val => {
if (!val || val.trim() === '') return undefined;
const num = Number(val);
return isNaN(num) ? undefined : num;
})
.refine(val => val === undefined || (typeof val === 'number' && val >= 1800 && val <= new Date().getFullYear()), {
message: "Founded year must be between 1800 and current year"
}),
headquarters_location: z.string().optional(),
@@ -70,10 +74,10 @@ export function OperatorForm({ onSubmit, onCancel, initialData }: OperatorFormPr
description: initialData?.description || '',
person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined,
founded_year: initialData?.founded_year ? String(initialData.founded_year) : '',
headquarters_location: initialData?.headquarters_location || '',
images: initialData?.images || { uploaded: [] }
}
} as Partial<OperatorFormData>
});

View File

@@ -22,8 +22,12 @@ const propertyOwnerSchema = z.object({
website_url: z.string().url().optional().or(z.literal('')),
founded_year: z.string()
.optional()
.transform(val => val === '' || val === undefined ? undefined : Number(val))
.refine(val => val === undefined || (!isNaN(val) && val >= 1800 && val <= new Date().getFullYear()), {
.transform(val => {
if (!val || val.trim() === '') return undefined;
const num = Number(val);
return isNaN(num) ? undefined : num;
})
.refine(val => val === undefined || (typeof val === 'number' && val >= 1800 && val <= new Date().getFullYear()), {
message: "Founded year must be between 1800 and current year"
}),
headquarters_location: z.string().optional(),
@@ -70,10 +74,10 @@ export function PropertyOwnerForm({ onSubmit, onCancel, initialData }: PropertyO
description: initialData?.description || '',
person_type: initialData?.person_type || 'company',
website_url: initialData?.website_url || '',
founded_year: initialData?.founded_year || undefined,
founded_year: initialData?.founded_year ? String(initialData.founded_year) : '',
headquarters_location: initialData?.headquarters_location || '',
images: initialData?.images || { uploaded: [] }
}
} as Partial<PropertyOwnerFormData>
});

View File

@@ -57,6 +57,24 @@ export function EntityMultiImageUploader({
}
}, [mode, entityId, entityType]);
// Cleanup blob URLs when component unmounts or images change
useEffect(() => {
const currentImages = value.uploaded;
return () => {
// Revoke all blob URLs on cleanup
currentImages.forEach(image => {
if (image.isLocal && image.url.startsWith('blob:')) {
try {
URL.revokeObjectURL(image.url);
} catch (error) {
console.error('Error revoking object URL:', error);
}
}
});
};
}, [value.uploaded]);
const fetchEntityPhotos = async () => {
setLoadingPhotos(true);
try {

View File

@@ -43,14 +43,15 @@ export function useEntityVersions(entityType: string, entityId: string) {
// Track the current channel to prevent duplicate subscriptions
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null);
// Track if a fetch is in progress to prevent race conditions
const fetchInProgressRef = useRef(false);
// Use a request counter to track the latest fetch and prevent race conditions
const requestCounterRef = useRef(0);
const fetchVersions = useCallback(async () => {
try {
if (!isMountedRef.current || fetchInProgressRef.current) return;
if (!isMountedRef.current) return;
fetchInProgressRef.current = true;
// Increment counter and capture the current request ID
const currentRequestId = ++requestCounterRef.current;
setLoading(true);
@@ -63,6 +64,9 @@ export function useEntityVersions(entityType: string, entityId: string) {
if (error) throw error;
// Only continue if this is still the latest request
if (currentRequestId !== requestCounterRef.current) return;
// Fetch profiles separately
const userIds = [...new Set(data?.map(v => v.changed_by).filter(Boolean) || [])];
const { data: profiles } = await supabase
@@ -70,6 +74,9 @@ export function useEntityVersions(entityType: string, entityId: string) {
.select('user_id, username, avatar_url')
.in('user_id', userIds);
// Check again if this is still the latest request
if (currentRequestId !== requestCounterRef.current) return;
const versionsWithProfiles = data?.map(v => {
const profile = profiles?.find(p => p.user_id === v.changed_by);
return {
@@ -81,19 +88,16 @@ export function useEntityVersions(entityType: string, entityId: string) {
};
}) as EntityVersion[];
// Only update state if component is still mounted
if (isMountedRef.current) {
// Only update state if component is still mounted and this is still the latest request
if (isMountedRef.current && currentRequestId === requestCounterRef.current) {
setVersions(versionsWithProfiles || []);
setCurrentVersion(versionsWithProfiles?.find(v => v.is_current) || null);
setLoading(false);
}
} catch (error: any) {
console.error('Error fetching versions:', error);
if (isMountedRef.current) {
toast.error('Failed to load version history');
}
} finally {
fetchInProgressRef.current = false;
if (isMountedRef.current) {
setLoading(false);
}
}

View File

@@ -58,14 +58,24 @@ export function useSearch(options: UseSearchOptions = {}) {
try {
const stored = localStorage.getItem('thrillwiki_recent_searches');
if (stored) {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
setRecentSearches(parsed);
try {
const parsed = JSON.parse(stored);
if (Array.isArray(parsed)) {
setRecentSearches(parsed);
} else {
// Invalid format, clear it
console.warn('Recent searches data is not an array, clearing');
localStorage.removeItem('thrillwiki_recent_searches');
}
} catch (parseError) {
// JSON parse failed, data is corrupted
console.error('Failed to parse recent searches from localStorage:', parseError);
localStorage.removeItem('thrillwiki_recent_searches');
}
}
} catch (error) {
console.error('Failed to parse recent searches from localStorage:', error);
localStorage.removeItem('thrillwiki_recent_searches');
// localStorage access failed
console.error('Error accessing localStorage:', error);
}
}, []);