feat: Enable TypeScript strict mode

This commit is contained in:
gpt-engineer-app[bot]
2025-11-03 00:58:42 +00:00
parent 061c06be29
commit d126be2908
15 changed files with 308 additions and 68 deletions

View File

@@ -79,7 +79,7 @@ interface ParkFormProps {
onSubmit: (data: ParkFormData & {
operator_id?: string;
property_owner_id?: string;
_compositeSubmission?: any;
_compositeSubmission?: import('@/types/composite-submission').ParkCompositeSubmission;
}) => Promise<void>;
onCancel?: () => void;
initialData?: Partial<ParkFormData & {
@@ -214,7 +214,7 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
}
// Build composite submission if new entities were created
const submissionContent: any = {
const submissionContent: import('@/types/composite-submission').ParkCompositeSubmission = {
park: data,
};

View File

@@ -90,7 +90,11 @@ export function ConflictResolutionDialog({
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium">
{item?.item_type.replace('_', ' ').toUpperCase()}: {item?.item_data.name}
{item?.item_type.replace('_', ' ').toUpperCase()}: {
item && typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data) && 'name' in item.item_data
? String((item.item_data as Record<string, unknown>).name)
: 'Unnamed'
}
</p>
<p className="text-sm mt-1">{conflict.message}</p>
</AlertDescription>

View File

@@ -24,8 +24,10 @@ export function DependencyTreeView({ items }: DependencyTreeViewProps) {
};
const getItemLabel = (item: SubmissionItemWithDeps): string => {
const data = item.item_data;
const name = data.name || 'Unnamed';
const data = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
? item.item_data as Record<string, unknown>
: {};
const name = 'name' in data && typeof data.name === 'string' ? data.name : 'Unnamed';
const type = item.item_type.replace('_', ' ');
return `${name} (${type})`;
};

View File

@@ -104,7 +104,9 @@ export function DependencyVisualizer({ items, selectedIds }: DependencyVisualize
</CardHeader>
<CardContent className={isMobile ? "p-4 pt-0" : ""}>
<p className={`font-medium ${isMobile ? 'text-sm' : 'text-sm'}`}>
{item.item_data.name || 'Unnamed'}
{typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data) && 'name' in item.item_data
? String((item.item_data as Record<string, unknown>).name)
: 'Unnamed'}
</p>
{item.dependents && item.dependents.length > 0 && (
<p className={`text-muted-foreground mt-1 ${isMobile ? 'text-xs' : 'text-xs'}`}>

View File

@@ -93,15 +93,21 @@ export function ItemEditDialog({ item, items, open, onOpenChange, onComplete }:
};
const handlePhotoSubmit = async (caption: string, credit: string) => {
const itemData = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
? item.item_data as Record<string, unknown>
: {};
const photos = 'photos' in itemData && Array.isArray(itemData.photos)
? itemData.photos
: [];
const photoData = {
...item.item_data,
photos: Array.isArray(item.item_data.photos)
? item.item_data.photos.map((photo: unknown) => ({
...(typeof photo === 'object' && photo !== null ? photo : {}),
...itemData,
photos: photos.map((photo: unknown) => ({
...(typeof photo === 'object' && photo !== null ? photo as Record<string, unknown> : {}),
caption,
credit,
}))
: [],
})),
};
await handleSubmit(photoData);
};

View File

@@ -127,7 +127,7 @@ export function ItemReviewCard({ item, onEdit, onStatusChange, submissionId }: I
<ValidationSummary
item={{
item_type: item.item_type,
item_data: item.item_data,
item_data: item.item_data as Record<string, unknown>,
id: item.id,
}}
onValidationChange={handleValidationChange}

View File

@@ -87,9 +87,9 @@ export function ItemSelectorDialog({
<span className="font-medium capitalize">
{item.item_type.replace('_', ' ')}
</span>
{item.item_data.name && (
{typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data) && 'name' in item.item_data && (
<span className="text-sm text-muted-foreground">
{String(item.item_data.name)}
{String((item.item_data as Record<string, unknown>).name)}
</span>
)}
{item.dependencies && item.dependencies.length > 0 && (

View File

@@ -145,7 +145,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
};
// Wrapped delete with confirmation
const handleDeleteSubmission = useCallback((item: any) => {
const handleDeleteSubmission = useCallback((item: { id: string; submission_type?: string }) => {
setConfirmDialog({
open: true,
title: 'Delete Submission',
@@ -198,7 +198,7 @@ export const ModerationQueue = forwardRef<ModerationQueueRef, ModerationQueuePro
enabled: true,
});
const handleOpenPhotos = (photos: any[], index: number) => {
const handleOpenPhotos = (photos: PhotoItem[], index: number) => {
setSelectedPhotos(photos);
setSelectedPhotoIndex(index);
setPhotoModalOpen(true);

View File

@@ -605,9 +605,12 @@ export function SubmissionReviewManager({
open={showValidationBlockerDialog}
onClose={() => setShowValidationBlockerDialog(false)}
blockingErrors={Array.from(validationResults.values()).flatMap(r => r.blockingErrors)}
itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i =>
i.item_data?.name || i.item_type.replace('_', ' ')
)}
itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => {
const name = typeof i.item_data === 'object' && i.item_data !== null && !Array.isArray(i.item_data) && 'name' in i.item_data
? String((i.item_data as Record<string, unknown>).name)
: i.item_type.replace('_', ' ');
return name;
})}
/>
<WarningConfirmDialog
@@ -619,9 +622,12 @@ export function SubmissionReviewManager({
handleApprove();
}}
warnings={Array.from(validationResults.values()).flatMap(r => r.warnings)}
itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i =>
i.item_data?.name || i.item_type.replace('_', ' ')
)}
itemNames={items.filter(i => selectedItemIds.has(i.id)).map(i => {
const name = typeof i.item_data === 'object' && i.item_data !== null && !Array.isArray(i.item_data) && 'name' in i.item_data
? String((i.item_data as Record<string, unknown>).name)
: i.item_type.replace('_', ' ');
return name;
})}
/>
<ConflictResolutionModal

View File

@@ -91,14 +91,7 @@ const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
import('@/types/recharts').TooltipProps
>(
(
{
@@ -115,7 +108,7 @@ const ChartTooltipContent = React.forwardRef<
color,
nameKey,
labelKey,
}: any,
},
ref,
) => {
const { config } = useChart();
@@ -163,7 +156,11 @@ const ChartTooltipContent = React.forwardRef<
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
const indicatorColor = color ||
(typeof item.payload === 'object' && item.payload !== null && 'fill' in item.payload
? (item.payload as Record<string, unknown>).fill as string
: undefined) ||
item.color;
return (
<div
@@ -229,12 +226,7 @@ const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
hideIcon?: boolean;
nameKey?: string;
payload?: any[];
verticalAlign?: "top" | "bottom";
}
import('@/types/recharts').LegendProps
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
const { config } = useChart();
@@ -247,7 +239,7 @@ const ChartLegendContent = React.forwardRef<
ref={ref}
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
>
{payload.map((item: any) => {
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);

View File

@@ -137,8 +137,8 @@ export function useEntityCache() {
}
try {
let data: any[] | null = null;
let error: any = null;
let data: unknown[] | null = null;
let error: unknown = null;
// Use type-safe table queries
switch (type) {
@@ -172,18 +172,20 @@ export function useEntityCache() {
}
if (error) {
logger.error(`Error fetching ${type}:`, error);
logger.error(`Error fetching ${type}:`, { error: getErrorMessage(error as Error) });
return [];
}
// Cache the fetched entities
if (data) {
data.forEach((entity: any) => {
setCached(type, entity.id, entity as EntityTypeMap[T]);
(data as Array<Record<string, unknown>>).forEach((entity) => {
if (entity && typeof entity === 'object' && 'id' in entity && 'name' in entity) {
setCached(type, entity.id as string, entity as EntityTypeMap[T]);
}
});
}
return data || [];
return (data as EntityTypeMap[T][]) || [];
} catch (error: unknown) {
logger.error(`Failed to bulk fetch ${type}:`, { error: getErrorMessage(error) });
return [];
@@ -194,7 +196,7 @@ export function useEntityCache() {
* Fetch and cache related entities based on submission content
* Automatically determines which entities to fetch from submission data
*/
const fetchRelatedEntities = useCallback(async (submissions: any[]): Promise<void> => {
const fetchRelatedEntities = useCallback(async (submissions: Array<{ content?: Record<string, string | number>; submission_type?: string }>): Promise<void> => {
const rideIds = new Set<string>();
const parkIds = new Set<string>();
const companyIds = new Set<string>();
@@ -203,20 +205,20 @@ export function useEntityCache() {
submissions.forEach(submission => {
const content = submission.content;
if (content && typeof content === 'object') {
if (content.ride_id) rideIds.add(content.ride_id);
if (content.park_id) parkIds.add(content.park_id);
if (content.company_id) companyIds.add(content.company_id);
if (content.entity_id) {
if (typeof content.ride_id === 'string') rideIds.add(content.ride_id);
if (typeof content.park_id === 'string') parkIds.add(content.park_id);
if (typeof content.company_id === 'string') companyIds.add(content.company_id);
if (typeof content.entity_id === 'string') {
if (submission.submission_type === 'ride') rideIds.add(content.entity_id);
if (submission.submission_type === 'park') parkIds.add(content.entity_id);
if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type)) {
if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(submission.submission_type || '')) {
companyIds.add(content.entity_id);
}
}
if (content.manufacturer_id) companyIds.add(content.manufacturer_id);
if (content.designer_id) companyIds.add(content.designer_id);
if (content.operator_id) companyIds.add(content.operator_id);
if (content.property_owner_id) companyIds.add(content.property_owner_id);
if (typeof content.manufacturer_id === 'string') companyIds.add(content.manufacturer_id);
if (typeof content.designer_id === 'string') companyIds.add(content.designer_id);
if (typeof content.operator_id === 'string') companyIds.add(content.operator_id);
if (typeof content.property_owner_id === 'string') companyIds.add(content.property_owner_id);
}
});

View File

@@ -4,14 +4,15 @@ import { logger } from './logger';
import { extractCloudflareImageId } from './cloudflareImageUtils';
// Core submission item interface with dependencies
// Type safety for item_data will be added in Phase 5 after fixing components
// NOTE: item_data and original_data use `unknown` because they contain dynamic structures
// that vary by item_type. Use type guards from @/types/submission-item-data.ts to access safely.
export interface SubmissionItemWithDeps {
id: string;
submission_id: string;
item_type: string;
item_data: any; // Complex nested structure - will be typed properly in Phase 5
original_data: any; // Complex nested structure - will be typed properly in Phase 5
item_data: unknown; // Dynamic structure - use type guards for safe access
original_data?: unknown; // Dynamic structure - use type guards for safe access
action_type?: 'create' | 'edit' | 'delete';
status: 'pending' | 'approved' | 'rejected' | 'flagged' | 'skipped'; // Matches ReviewStatus from statuses.ts
depends_on: string | null;
@@ -43,7 +44,10 @@ export interface ConflictCheckResult {
serverVersion?: {
last_modified_at: string;
last_modified_by: string;
modified_by_profile?: any;
modified_by_profile?: {
username: string;
display_name?: string;
};
} | null;
}
@@ -110,18 +114,28 @@ export async function detectDependencyConflicts(
// Suggest creating parent
if (parent.status !== 'rejected') {
const parentData = typeof parent.item_data === 'object' && parent.item_data !== null && !Array.isArray(parent.item_data)
? parent.item_data as Record<string, unknown>
: {};
const parentName = 'name' in parentData && typeof parentData.name === 'string' ? parentData.name : 'Unnamed';
suggestions.push({
action: 'create_parent',
label: `Also approve ${parent.item_type}: ${parent.item_data.name}`,
label: `Also approve ${parent.item_type}: ${parentName}`,
});
}
// Suggest linking to existing entity
if (parent.item_type === 'park') {
const parentData = typeof parent.item_data === 'object' && parent.item_data !== null && !Array.isArray(parent.item_data)
? parent.item_data as Record<string, unknown>
: {};
const parentName = 'name' in parentData && typeof parentData.name === 'string' ? parentData.name : '';
const { data: parks } = await supabase
.from('parks')
.select('id, name')
.ilike('name', `%${parent.item_data.name}%`)
.ilike('name', `%${parentName}%`)
.limit(3);
parks?.forEach(park => {
@@ -189,11 +203,15 @@ export async function approveSubmissionItems(
try {
// Determine if this is an edit by checking for entity_id in item_data
const itemData = typeof item.item_data === 'object' && item.item_data !== null && !Array.isArray(item.item_data)
? item.item_data as Record<string, unknown>
: {};
isEdit = !!(
item.item_data.park_id ||
item.item_data.ride_id ||
item.item_data.company_id ||
item.item_data.ride_model_id
('park_id' in itemData && itemData.park_id) ||
('ride_id' in itemData && itemData.ride_id) ||
('company_id' in itemData && itemData.company_id) ||
('ride_model_id' in itemData && itemData.ride_model_id)
);
// Create the entity based on type with dependency resolution

View File

@@ -0,0 +1,40 @@
/**
* Type definitions for composite submissions
* Used when creating multiple related entities in a single submission
*/
import type { TempCompanyData } from './company';
/**
* Composite submission structure for park creation with related entities
*/
export interface ParkCompositeSubmission {
park: {
name: string;
slug: string;
description?: string;
park_type: string;
status: string;
opening_date?: string;
closing_date?: string;
location_id?: string;
website_url?: string;
phone?: string;
email?: string;
operator_id?: string | null;
property_owner_id?: string | null;
[key: string]: unknown;
};
new_operator?: TempCompanyData;
new_property_owner?: TempCompanyData;
}
/**
* Generic composite submission content type
* Supports any entity type with optional related entities
*/
export interface CompositeSubmissionContent {
[entityKey: string]: {
[key: string]: unknown;
} | TempCompanyData | undefined;
}

55
src/types/recharts.ts Normal file
View File

@@ -0,0 +1,55 @@
/**
* Type definitions for Recharts payloads
* Provides type-safe alternatives to `any` in chart components
*/
import type { ReactNode } from 'react';
/**
* Generic chart payload item structure
*/
export interface ChartPayloadItem<T = unknown> {
value?: number | string;
name?: string;
dataKey?: string;
color?: string;
fill?: string;
payload?: T;
[key: string]: unknown;
}
/**
* Tooltip content props from Recharts
*/
export interface TooltipProps<T = unknown> {
active?: boolean;
payload?: ChartPayloadItem<T>[];
label?: string | number;
labelFormatter?: (value: unknown, payload: ChartPayloadItem<T>[]) => ReactNode;
formatter?: (
value: number | string,
name: string,
item: ChartPayloadItem<T>,
index: number,
payload: unknown
) => ReactNode;
className?: string;
indicator?: 'line' | 'dot' | 'dashed';
hideLabel?: boolean;
hideIndicator?: boolean;
color?: string;
nameKey?: string;
labelKey?: string;
labelClassName?: string;
}
/**
* Legend content props from Recharts
*/
export interface LegendProps<T = unknown> {
payload?: ChartPayloadItem<T>[];
className?: string;
hideIcon?: boolean;
nameKey?: string;
verticalAlign?: 'top' | 'bottom';
}

View File

@@ -0,0 +1,113 @@
/**
* Type-safe helpers for accessing submission item data
* Provides type guards and type narrowing for Json fields
*/
import type { Json } from '@/integrations/supabase/types';
/**
* Base structure that all item_data objects should have
*/
export interface BaseItemData {
name?: string;
slug?: string;
description?: string;
[key: string]: Json;
}
/**
* Type guard to safely check if Json is an object with a name property
*/
export function hasName(data: Json): data is { name: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'name' in data && typeof (data as Record<string, Json>).name === 'string';
}
/**
* Type guard to check if Json is a photos array
*/
export function hasPhotos(data: Json): data is { photos: Array<Record<string, Json>> } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'photos' in data && Array.isArray((data as Record<string, Json>).photos);
}
/**
* Type guard for manufacturer data
*/
export function hasManufacturer(data: Json): data is { manufacturer_id: string; manufacturer_name: string } & Record<string, Json> {
return (
typeof data === 'object' &&
data !== null &&
!Array.isArray(data) &&
'manufacturer_id' in data &&
'manufacturer_name' in data
);
}
/**
* Type guard for park data
*/
export function hasParkId(data: Json): data is { park_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'park_id' in data;
}
/**
* Type guard for ride data
*/
export function hasRideId(data: Json): data is { ride_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'ride_id' in data;
}
/**
* Type guard for company data
*/
export function hasCompanyId(data: Json): data is { company_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'company_id' in data;
}
/**
* Type guard for ride model data
*/
export function hasRideModelId(data: Json): data is { ride_model_id: string } & Record<string, Json> {
return typeof data === 'object' && data !== null && !Array.isArray(data) && 'ride_model_id' in data;
}
/**
* Safely get name from item_data
*/
export function getItemName(data: Json): string {
if (hasName(data)) {
return data.name;
}
return 'Unnamed';
}
/**
* Safely get photos from item_data
*/
export function getItemPhotos(data: Json): Array<Record<string, Json>> {
if (hasPhotos(data)) {
return data.photos;
}
return [];
}
/**
* Convert Json to a record for form usage
* Only use when you need to pass data to form components
*/
export function jsonToRecord(data: Json): Record<string, Json> {
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
return data as Record<string, Json>;
}
return {};
}
/**
* Type-safe way to access nested Json properties
*/
export function getProperty<T = Json>(data: Json, key: string): T | undefined {
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
const obj = data as Record<string, Json>;
return obj[key] as T | undefined;
}
return undefined;
}