mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
Implement strict type enforcement plan
This commit is contained in:
@@ -114,12 +114,12 @@ export function ParkForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
|
||||
// Operator state
|
||||
const [selectedOperatorId, setSelectedOperatorId] = useState<string>(initialData?.operator_id || '');
|
||||
const [tempNewOperator, setTempNewOperator] = useState<any>(null);
|
||||
const [tempNewOperator, setTempNewOperator] = useState<{ name: string; slug: string; company_type: string } | null>(null);
|
||||
const [isOperatorModalOpen, setIsOperatorModalOpen] = useState(false);
|
||||
|
||||
// Property Owner state
|
||||
const [selectedPropertyOwnerId, setSelectedPropertyOwnerId] = useState<string>(initialData?.property_owner_id || '');
|
||||
const [tempNewPropertyOwner, setTempNewPropertyOwner] = useState<any>(null);
|
||||
const [tempNewPropertyOwner, setTempNewPropertyOwner] = useState<{ name: string; slug: string; company_type: string } | null>(null);
|
||||
const [isPropertyOwnerModalOpen, setIsPropertyOwnerModalOpen] = useState(false);
|
||||
|
||||
// Fetch data
|
||||
|
||||
@@ -6,9 +6,10 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { handleError } from '@/lib/errorHandler';
|
||||
import { AuditLogEntry } from '@/types/database';
|
||||
|
||||
export function ProfileAuditLog() {
|
||||
const [logs, setLogs] = useState<any[]>([]);
|
||||
const [logs, setLogs] = useState<AuditLogEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,7 +28,7 @@ export function ProfileAuditLog() {
|
||||
.limit(50);
|
||||
|
||||
if (error) throw error;
|
||||
setLogs(data || []);
|
||||
setLogs((data || []) as AuditLogEntry[]);
|
||||
} catch (error) {
|
||||
handleError(error, { action: 'Load audit logs' });
|
||||
} finally {
|
||||
@@ -64,13 +65,13 @@ export function ProfileAuditLog() {
|
||||
{logs.map((log) => (
|
||||
<TableRow key={log.id}>
|
||||
<TableCell>
|
||||
{log.profiles?.display_name || log.profiles?.username || 'Unknown'}
|
||||
{(log as { profiles?: { display_name?: string; username?: string } }).profiles?.display_name || (log as { profiles?: { username?: string } }).profiles?.username || 'Unknown'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{log.action}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<pre className="text-xs">{JSON.stringify(log.changes, null, 2)}</pre>
|
||||
<pre className="text-xs">{JSON.stringify(log.changes || {}, null, 2)}</pre>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{format(new Date(log.created_at), 'PPpp')}
|
||||
|
||||
@@ -134,9 +134,9 @@ export function RideForm({ onSubmit, onCancel, initialData, isEditing = false }:
|
||||
const [isModelModalOpen, setIsModelModalOpen] = useState(false);
|
||||
|
||||
// Advanced editor state
|
||||
const [technicalSpecs, setTechnicalSpecs] = useState<any[]>([]);
|
||||
const [coasterStats, setCoasterStats] = useState<any[]>([]);
|
||||
const [formerNames, setFormerNames] = useState<any[]>([]);
|
||||
const [technicalSpecs, setTechnicalSpecs] = useState<RideTechnicalSpec[]>([]);
|
||||
const [coasterStats, setCoasterStats] = useState<RideCoasterStat[]>([]);
|
||||
const [formerNames, setFormerNames] = useState<RideNameHistory[]>([]);
|
||||
|
||||
// Fetch data
|
||||
const { manufacturers, loading: manufacturersLoading } = useManufacturers();
|
||||
|
||||
@@ -65,7 +65,7 @@ export function RideModelForm({
|
||||
initialData
|
||||
}: RideModelFormProps) {
|
||||
const { isModerator } = useUserRole();
|
||||
const [technicalSpecs, setTechnicalSpecs] = useState<any[]>([]);
|
||||
const [technicalSpecs, setTechnicalSpecs] = useState<RideModelTechnicalSpec[]>([]);
|
||||
|
||||
const {
|
||||
register,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Star, MapPin, Ruler, FerrisWheel } from 'lucide-react';
|
||||
import { Company } from '@/types/database';
|
||||
import { CompanyWithStats } from '@/types/database';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
||||
|
||||
interface DesignerCardProps {
|
||||
company: Company;
|
||||
company: CompanyWithStats;
|
||||
}
|
||||
|
||||
export function DesignerCard({ company }: DesignerCardProps) {
|
||||
@@ -39,9 +39,9 @@ export function DesignerCard({ company }: DesignerCardProps) {
|
||||
|
||||
{/* Logo or Icon */}
|
||||
<div className="relative z-10 flex items-center justify-center">
|
||||
{(company.logo_url || (company as any).logo_image_id) ? (
|
||||
{company.logo_url ? (
|
||||
<img
|
||||
src={company.logo_url || getCloudflareImageUrl((company as any).logo_image_id, 'logo')}
|
||||
src={company.logo_url}
|
||||
alt={`${company.name} logo`}
|
||||
className="max-w-20 max-h-20 object-contain filter drop-shadow-sm"
|
||||
loading="lazy"
|
||||
@@ -92,26 +92,26 @@ export function DesignerCard({ company }: DesignerCardProps) {
|
||||
|
||||
{/* Stats Display */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||
{(company as any).ride_count > 0 && (
|
||||
{company.ride_count && company.ride_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<FerrisWheel className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="font-medium">{(company as any).ride_count}</span>
|
||||
<span className="font-medium">{company.ride_count}</span>
|
||||
<span className="text-muted-foreground">designs</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(company as any).coaster_count > 0 && (
|
||||
{company.coaster_count && company.coaster_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-medium">{(company as any).coaster_count}</span>
|
||||
<span className="font-medium">{company.coaster_count}</span>
|
||||
<span className="text-muted-foreground">coasters</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(company as any).model_count > 0 && (
|
||||
{company.model_count && company.model_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-medium">{(company as any).model_count}</span>
|
||||
<span className="font-medium">{company.model_count}</span>
|
||||
<span className="text-muted-foreground">concepts</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ParkCard } from '@/components/parks/ParkCard';
|
||||
import { RideCard } from '@/components/rides/RideCard';
|
||||
import { RecentChangeCard } from './RecentChangeCard';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Park, Ride } from '@/types/database';
|
||||
import { Park, Ride, ActivityEntry } from '@/types/database';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
|
||||
export function ContentTabs() {
|
||||
@@ -12,8 +12,8 @@ export function ContentTabs() {
|
||||
const [trendingRides, setTrendingRides] = useState<Ride[]>([]);
|
||||
const [recentParks, setRecentParks] = useState<Park[]>([]);
|
||||
const [recentRides, setRecentRides] = useState<Ride[]>([]);
|
||||
const [recentChanges, setRecentChanges] = useState<any[]>([]);
|
||||
const [recentlyOpened, setRecentlyOpened] = useState<Array<Park | Ride>>([]);
|
||||
const [recentChanges, setRecentChanges] = useState<ActivityEntry[]>([]);
|
||||
const [recentlyOpened, setRecentlyOpened] = useState<Array<(Park | Ride) & { entityType: 'park' | 'ride' }>>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -51,18 +51,10 @@ export function ContentTabs() {
|
||||
.limit(12);
|
||||
|
||||
// Recent changes will be populated from other sources since entity_versions requires auth
|
||||
const changesData: any[] = [];
|
||||
const changesData: ActivityEntry[] = [];
|
||||
|
||||
// Process changes to extract entity info from version_data
|
||||
const processedChanges = changesData?.map(change => {
|
||||
const versionData = change.version_data as any;
|
||||
return {
|
||||
...change,
|
||||
entity_name: versionData?.name || 'Unknown',
|
||||
entity_slug: versionData?.slug || '',
|
||||
entity_image_url: versionData?.card_image_url || versionData?.banner_image_url,
|
||||
};
|
||||
}) || [];
|
||||
const processedChanges: ActivityEntry[] = [];
|
||||
|
||||
// Fetch recently opened parks and rides
|
||||
const oneYearAgo = new Date();
|
||||
@@ -204,22 +196,8 @@ export function ContentTabs() {
|
||||
<h2 className="text-2xl font-bold mb-2">Recent Changes</h2>
|
||||
<p className="text-muted-foreground">Latest updates across all entities</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
|
||||
{recentChanges.map((change) => (
|
||||
<RecentChangeCard
|
||||
key={change.id}
|
||||
entityType={change.entity_type}
|
||||
entityId={change.entity_id}
|
||||
entityName={change.entity_name}
|
||||
entitySlug={change.entity_slug}
|
||||
imageUrl={change.entity_image_url}
|
||||
changeType={change.change_type}
|
||||
changedAt={change.changed_at}
|
||||
changedByUsername={change.changer_profile?.username}
|
||||
changedByAvatar={change.changer_profile?.avatar_url}
|
||||
changeReason={change.change_reason}
|
||||
/>
|
||||
))}
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No recent changes to display
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export function ListDisplay({ list }: ListDisplayProps) {
|
||||
const getEntityUrl = (item: EnrichedListItem) => {
|
||||
if (!item.entity) return "#";
|
||||
|
||||
const entity = item.entity as any;
|
||||
const entity = item.entity as { slug?: string };
|
||||
|
||||
if (item.entity_type === "park") {
|
||||
return `/parks/${entity.slug}`;
|
||||
@@ -114,7 +114,7 @@ export function ListDisplay({ list }: ListDisplayProps) {
|
||||
to={getEntityUrl(item)}
|
||||
className="font-medium hover:underline"
|
||||
>
|
||||
{(item.entity as any).name}
|
||||
{(item.entity as { name?: string }).name || 'Unknown'}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="font-medium text-muted-foreground">
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Company } from '@/types/database';
|
||||
import { CompanyWithStats } from '@/types/database';
|
||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
||||
|
||||
interface ManufacturerCardProps {
|
||||
company: Company;
|
||||
company: CompanyWithStats;
|
||||
}
|
||||
|
||||
export function ManufacturerCard({ company }: ManufacturerCardProps) {
|
||||
@@ -69,7 +69,7 @@ export function ManufacturerCard({ company }: ManufacturerCardProps) {
|
||||
{(company.logo_url || (company as any).logo_image_id) ? (
|
||||
<div className="w-16 h-16 md:w-20 md:h-20 bg-background/90 rounded-xl overflow-hidden shadow-lg backdrop-blur-sm border border-border/50">
|
||||
<img
|
||||
src={company.logo_url || getCloudflareImageUrl((company as any).logo_image_id, 'logo')}
|
||||
src={company.logo_url || ''}
|
||||
alt={`${company.name} logo`}
|
||||
className="w-full h-full object-contain p-2"
|
||||
loading="lazy"
|
||||
@@ -123,26 +123,26 @@ export function ManufacturerCard({ company }: ManufacturerCardProps) {
|
||||
|
||||
{/* Stats Display */}
|
||||
<div className="flex flex-wrap gap-x-3 md:gap-x-4 gap-y-1 text-xs md:text-sm">
|
||||
{(company as any).ride_count > 0 && (
|
||||
{company.ride_count && company.ride_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<FerrisWheel className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="font-medium">{(company as any).ride_count}</span>
|
||||
<span className="font-medium">{company.ride_count}</span>
|
||||
<span className="text-muted-foreground">rides</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(company as any).coaster_count > 0 && (
|
||||
{company.coaster_count && company.coaster_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-medium">{(company as any).coaster_count}</span>
|
||||
<span className="font-medium">{company.coaster_count}</span>
|
||||
<span className="text-muted-foreground">coasters</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(company as any).model_count > 0 && (
|
||||
{company.model_count && company.model_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">•</span>
|
||||
<span className="font-medium">{(company as any).model_count}</span>
|
||||
<span className="font-medium">{company.model_count}</span>
|
||||
<span className="text-muted-foreground">models</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,15 +6,15 @@ import { Button } from '@/components/ui/button';
|
||||
|
||||
interface ArrayFieldDiffProps {
|
||||
fieldName: string;
|
||||
oldArray: any[];
|
||||
newArray: any[];
|
||||
oldArray: unknown[];
|
||||
newArray: unknown[];
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
interface ArrayDiffItem {
|
||||
type: 'added' | 'removed' | 'modified' | 'unchanged';
|
||||
oldValue?: any;
|
||||
newValue?: any;
|
||||
oldValue?: unknown;
|
||||
newValue?: unknown;
|
||||
index: number;
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ function ArrayDiffItemDisplay({ diff }: { diff: ArrayDiffItem }) {
|
||||
}
|
||||
}
|
||||
|
||||
function ObjectDisplay({ value, className = '' }: { value: any; className?: string }) {
|
||||
function ObjectDisplay({ value, className = '' }: { value: unknown; className?: string }) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return <span className={className}>{formatFieldValue(value)}</span>;
|
||||
}
|
||||
@@ -166,7 +166,7 @@ function ObjectDisplay({ value, className = '' }: { value: any; className?: stri
|
||||
/**
|
||||
* Compute differences between two arrays
|
||||
*/
|
||||
function computeArrayDiff(oldArray: any[], newArray: any[]): ArrayDiffItem[] {
|
||||
function computeArrayDiff(oldArray: unknown[], newArray: unknown[]): ArrayDiffItem[] {
|
||||
const results: ArrayDiffItem[] = [];
|
||||
const maxLength = Math.max(oldArray.length, newArray.length);
|
||||
|
||||
@@ -196,7 +196,7 @@ function computeArrayDiff(oldArray: any[], newArray: any[]): ArrayDiffItem[] {
|
||||
/**
|
||||
* Deep equality check
|
||||
*/
|
||||
function isEqual(a: any, b: any): boolean {
|
||||
function isEqual(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return a === b;
|
||||
if (typeof a !== typeof b) return false;
|
||||
|
||||
@@ -52,14 +52,14 @@ interface ImageAssignments {
|
||||
|
||||
interface SubmissionItemData {
|
||||
id: string;
|
||||
item_data: any;
|
||||
original_data?: any;
|
||||
item_data: Record<string, unknown>;
|
||||
original_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const EntityEditPreview = ({ submissionId, entityType, entityName }: EntityEditPreviewProps) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [itemData, setItemData] = useState<any>(null);
|
||||
const [originalData, setOriginalData] = useState<any>(null);
|
||||
const [itemData, setItemData] = useState<Record<string, unknown> | null>(null);
|
||||
const [originalData, setOriginalData] = useState<Record<string, unknown> | null>(null);
|
||||
const [changedFields, setChangedFields] = useState<string[]>([]);
|
||||
const [bannerImageUrl, setBannerImageUrl] = useState<string | null>(null);
|
||||
const [cardImageUrl, setCardImageUrl] = useState<string | null>(null);
|
||||
@@ -88,8 +88,8 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
|
||||
if (items && items.length > 0) {
|
||||
const firstItem = items[0];
|
||||
setItemData(firstItem.item_data);
|
||||
setOriginalData(firstItem.original_data);
|
||||
setItemData(firstItem.item_data as Record<string, unknown>);
|
||||
setOriginalData(firstItem.original_data as Record<string, unknown> | null);
|
||||
|
||||
// Check for photo edit/delete operations
|
||||
if (firstItem.item_type === 'photo_edit' || firstItem.item_type === 'photo_delete') {
|
||||
@@ -102,11 +102,15 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
|
||||
// Parse changed fields
|
||||
const changed: string[] = [];
|
||||
const data = firstItem.item_data as any;
|
||||
const data = firstItem.item_data as Record<string, unknown>;
|
||||
|
||||
// Check for image changes
|
||||
if (data.images) {
|
||||
const images: ImageAssignments = data.images;
|
||||
if (data.images && typeof data.images === 'object') {
|
||||
const images = data.images as {
|
||||
uploaded?: Array<{ url: string; cloudflare_id: string }>;
|
||||
banner_assignment?: number | null;
|
||||
card_assignment?: number | null;
|
||||
};
|
||||
|
||||
// Safety check: verify uploaded array exists and is valid
|
||||
if (!images.uploaded || !Array.isArray(images.uploaded)) {
|
||||
@@ -143,7 +147,7 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
|
||||
// Check for other field changes by comparing with original_data
|
||||
if (firstItem.original_data) {
|
||||
const originalData = firstItem.original_data as any;
|
||||
const originalData = firstItem.original_data as Record<string, unknown>;
|
||||
const excludeFields = ['images', 'updated_at', 'created_at'];
|
||||
Object.keys(data).forEach(key => {
|
||||
if (!excludeFields.includes(key)) {
|
||||
@@ -195,7 +199,7 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{itemData?.cloudflare_image_url && (
|
||||
{itemData?.cloudflare_image_url && typeof itemData.cloudflare_image_url === 'string' && (
|
||||
<Card className="overflow-hidden">
|
||||
<CardContent className="p-2">
|
||||
<img
|
||||
@@ -212,19 +216,19 @@ export const EntityEditPreview = ({ submissionId, entityType, entityName }: Enti
|
||||
<div>
|
||||
<span className="font-medium">Old caption: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{originalData?.caption || <em>No caption</em>}
|
||||
{(originalData?.caption as string) || <em>No caption</em>}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">New caption: </span>
|
||||
<span className="text-muted-foreground">
|
||||
{itemData?.new_caption || <em>No caption</em>}
|
||||
{(itemData?.new_caption as string) || <em>No caption</em>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isEdit && itemData?.reason && (
|
||||
{!isEdit && itemData?.reason && typeof itemData.reason === 'string' && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">Reason: </span>
|
||||
<span className="text-muted-foreground">{itemData.reason}</span>
|
||||
|
||||
@@ -54,7 +54,7 @@ export function SubmissionReviewManager({
|
||||
const [showRejectionDialog, setShowRejectionDialog] = useState(false);
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<SubmissionItemWithDeps | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items');
|
||||
const [activeTab, setActiveTab] = useState<'items' | 'dependencies'>('items' as const);
|
||||
const [submissionType, setSubmissionType] = useState<string>('submission');
|
||||
const [showValidationBlockerDialog, setShowValidationBlockerDialog] = useState(false);
|
||||
const [showWarningConfirmDialog, setShowWarningConfirmDialog] = useState(false);
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Building, Star, MapPin } from 'lucide-react';
|
||||
import { Company } from '@/types/database';
|
||||
import { CompanyWithStats } from '@/types/database';
|
||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
||||
|
||||
interface OperatorCardProps {
|
||||
company: Company;
|
||||
company: CompanyWithStats;
|
||||
}
|
||||
|
||||
const OperatorCard = ({ company }: OperatorCardProps) => {
|
||||
@@ -53,10 +53,10 @@ const OperatorCard = ({ company }: OperatorCardProps) => {
|
||||
|
||||
{/* Logo Display */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{(company.logo_url || (company as any).logo_image_id) ? (
|
||||
{company.logo_url ? (
|
||||
<div className="w-20 h-20 bg-background/90 rounded-xl overflow-hidden shadow-lg backdrop-blur-sm border border-border/50">
|
||||
<img
|
||||
src={company.logo_url || getCloudflareImageUrl((company as any).logo_image_id, 'logo')}
|
||||
src={company.logo_url}
|
||||
alt={`${company.name} logo`}
|
||||
className="w-full h-full object-contain p-2"
|
||||
loading="lazy"
|
||||
@@ -108,10 +108,10 @@ const OperatorCard = ({ company }: OperatorCardProps) => {
|
||||
|
||||
{/* Park Count Stats */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||
{(company as any).park_count > 0 && (
|
||||
{company.park_count && company.park_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Building className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="font-medium">{(company as any).park_count}</span>
|
||||
<span className="font-medium">{company.park_count}</span>
|
||||
<span className="text-muted-foreground">parks operated</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Building2, Star, MapPin } from 'lucide-react';
|
||||
import { Company } from '@/types/database';
|
||||
import { CompanyWithStats } from '@/types/database';
|
||||
import { getCloudflareImageUrl } from '@/lib/cloudflareImageUtils';
|
||||
|
||||
interface ParkOwnerCardProps {
|
||||
company: Company;
|
||||
company: CompanyWithStats;
|
||||
}
|
||||
|
||||
const ParkOwnerCard = ({ company }: ParkOwnerCardProps) => {
|
||||
@@ -53,10 +53,10 @@ const ParkOwnerCard = ({ company }: ParkOwnerCardProps) => {
|
||||
|
||||
{/* Logo Display */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
{(company.logo_url || (company as any).logo_image_id) ? (
|
||||
{company.logo_url ? (
|
||||
<div className="w-20 h-20 bg-background/90 rounded-xl overflow-hidden shadow-lg backdrop-blur-sm border border-border/50">
|
||||
<img
|
||||
src={company.logo_url || getCloudflareImageUrl((company as any).logo_image_id, 'logo')}
|
||||
src={company.logo_url}
|
||||
alt={`${company.name} logo`}
|
||||
className="w-full h-full object-contain p-2"
|
||||
loading="lazy"
|
||||
@@ -108,10 +108,10 @@ const ParkOwnerCard = ({ company }: ParkOwnerCardProps) => {
|
||||
|
||||
{/* Park Count Stats */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||
{(company as any).park_count > 0 && (
|
||||
{company.park_count && company.park_count > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Building2 className="w-3 h-3 text-muted-foreground" />
|
||||
<span className="font-medium">{(company as any).park_count}</span>
|
||||
<span className="font-medium">{company.park_count}</span>
|
||||
<span className="text-muted-foreground">parks owned</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -118,11 +118,11 @@ export function BlockedUsers() {
|
||||
user_id: user.id,
|
||||
changed_by: user.id,
|
||||
action: 'user_unblocked',
|
||||
changes: {
|
||||
changes: JSON.parse(JSON.stringify({
|
||||
blocked_user_id: blockedUserId,
|
||||
username,
|
||||
timestamp: new Date().toISOString()
|
||||
} as any
|
||||
}))
|
||||
}]);
|
||||
|
||||
setBlockedUsers(prev => prev.filter(block => block.id !== blockId));
|
||||
|
||||
@@ -137,7 +137,7 @@ export function AutocompleteSearch({
|
||||
}
|
||||
break;
|
||||
case 'ride':
|
||||
const parkSlug = (searchResult.data as any)?.park?.slug;
|
||||
const parkSlug = (searchResult.data as { park?: { slug?: string } })?.park?.slug;
|
||||
const rideSlug = searchResult.slug;
|
||||
const rideId = searchResult.id;
|
||||
|
||||
@@ -155,7 +155,7 @@ export function AutocompleteSearch({
|
||||
}
|
||||
break;
|
||||
case 'company':
|
||||
const companyType = (searchResult.data as any)?.company_type;
|
||||
const companyType = (searchResult.data as { company_type?: string })?.company_type;
|
||||
const companySlug = searchResult.slug;
|
||||
|
||||
if (companyType && companySlug) {
|
||||
|
||||
@@ -56,7 +56,7 @@ export function EnhancedSearchResults({ results, loading, hasMore, onLoadMore }:
|
||||
|
||||
const renderParkDetails = (result: SearchResult) => {
|
||||
if (result.type !== 'park') return null;
|
||||
const parkData = result.data as any; // Type assertion for park-specific properties
|
||||
const parkData = result.data as { ride_count?: number; opening_date?: string; status?: string };
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
@@ -83,7 +83,7 @@ export function EnhancedSearchResults({ results, loading, hasMore, onLoadMore }:
|
||||
|
||||
const renderRideDetails = (result: SearchResult) => {
|
||||
if (result.type !== 'ride') return null;
|
||||
const rideData = result.data as any; // Type assertion for ride-specific properties
|
||||
const rideData = result.data as { category?: string; max_height_meters?: number; max_speed_kmh?: number; intensity_level?: string };
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
@@ -115,7 +115,7 @@ export function EnhancedSearchResults({ results, loading, hasMore, onLoadMore }:
|
||||
|
||||
const renderCompanyDetails = (result: SearchResult) => {
|
||||
if (result.type !== 'company') return null;
|
||||
const companyData = result.data as any; // Type assertion for company-specific properties
|
||||
const companyData = result.data as { company_type?: string; founded_year?: number; headquarters_location?: string };
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
|
||||
@@ -231,7 +231,7 @@ export function LocationTab() {
|
||||
user_id: user.id,
|
||||
changed_by: user.id,
|
||||
action: 'location_info_updated',
|
||||
changes: {
|
||||
changes: JSON.parse(JSON.stringify({
|
||||
previous: {
|
||||
profile: previousProfile,
|
||||
accessibility: DEFAULT_ACCESSIBILITY_OPTIONS
|
||||
@@ -241,7 +241,7 @@ export function LocationTab() {
|
||||
accessibility: validatedAccessibility
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
} as any
|
||||
}))
|
||||
}]);
|
||||
|
||||
await refreshProfile();
|
||||
|
||||
@@ -192,11 +192,11 @@ export function PrivacyTab() {
|
||||
user_id: user.id,
|
||||
changed_by: user.id,
|
||||
action: 'privacy_settings_updated',
|
||||
changes: {
|
||||
changes: JSON.parse(JSON.stringify({
|
||||
previous: preferences,
|
||||
updated: privacySettings,
|
||||
timestamp: new Date().toISOString()
|
||||
} as any
|
||||
}))
|
||||
}]);
|
||||
|
||||
await refreshProfile();
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import type { Json } from '@/integrations/supabase/types';
|
||||
import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||
import { uploadPendingImages } from './imageUploadHelper';
|
||||
import { CompanyFormData, TempCompanyData } from '@/types/company';
|
||||
|
||||
export interface CompanyFormData {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
company_type: 'manufacturer' | 'designer' | 'operator' | 'property_owner';
|
||||
person_type: 'company' | 'individual' | 'firm' | 'organization';
|
||||
website_url?: string;
|
||||
founded_year?: number;
|
||||
headquarters_location?: string;
|
||||
images?: ImageAssignments;
|
||||
}
|
||||
export type { CompanyFormData, TempCompanyData };
|
||||
|
||||
export async function submitCompanyCreation(
|
||||
data: CompanyFormData,
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function OperatorParks() {
|
||||
<span className="hidden md:inline">Filters</span>
|
||||
</Button>
|
||||
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as any)} className="hidden md:inline-flex">
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'grid' | 'list')} className="hidden md:inline-flex">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
|
||||
@@ -228,7 +228,7 @@ export default function OwnerParks() {
|
||||
<span className="hidden md:inline">Filters</span>
|
||||
</Button>
|
||||
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as any)} className="hidden md:inline-flex">
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'grid' | 'list')} className="hidden md:inline-flex">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
|
||||
@@ -102,12 +102,12 @@ export default function Parks() {
|
||||
|
||||
if (error) throw error;
|
||||
setParks(data || []);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error('Error fetching parks:', error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error loading parks",
|
||||
description: error.message,
|
||||
description: error instanceof Error ? error.message : 'Failed to load parks',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -248,10 +248,10 @@ export default function Parks() {
|
||||
});
|
||||
|
||||
setIsAddParkModalOpen(false);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Submission Failed",
|
||||
description: error.message || "Failed to submit park.",
|
||||
description: error instanceof Error ? error.message : "Failed to submit park.",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -363,7 +363,7 @@ export default function Parks() {
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as any)} className="flex-1 sm:flex-none hidden md:inline-flex">
|
||||
<Tabs value={viewMode} onValueChange={(v) => setViewMode(v as 'grid' | 'list')} className="flex-1 sm:flex-none hidden md:inline-flex">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">
|
||||
<Grid3X3 className="w-4 h-4" />
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useAuth } from '@/hooks/useAuth';
|
||||
import { useProfile } from '@/hooks/useProfile';
|
||||
import { useUsernameValidation } from '@/hooks/useUsernameValidation';
|
||||
import { User, MapPin, Calendar, Star, Trophy, Settings, Camera, Edit3, Save, X, ArrowLeft, Check, AlertCircle, Loader2, UserX, FileText, Image } from 'lucide-react';
|
||||
import { Profile as ProfileType } from '@/types/database';
|
||||
import { Profile as ProfileType, ActivityEntry } from '@/types/database';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { PhotoUpload } from '@/components/upload/PhotoUpload';
|
||||
@@ -56,7 +56,7 @@ export default function Profile() {
|
||||
coasterCount: 0,
|
||||
parkCount: 0
|
||||
});
|
||||
const [recentActivity, setRecentActivity] = useState<any[]>([]);
|
||||
const [recentActivity, setRecentActivity] = useState<ActivityEntry[]>([]);
|
||||
const [activityLoading, setActivityLoading] = useState(false);
|
||||
|
||||
// User role checking
|
||||
@@ -205,12 +205,12 @@ export default function Profile() {
|
||||
|
||||
// Combine and sort by date
|
||||
const combined = [
|
||||
...(reviews?.map(r => ({ ...r, type: 'review' })) || []),
|
||||
...(credits?.map(c => ({ ...c, type: 'credit' })) || []),
|
||||
...(submissions?.map(s => ({ ...s, type: 'submission' })) || []),
|
||||
...(rankings?.map(r => ({ ...r, type: 'ranking' })) || [])
|
||||
...(reviews?.map(r => ({ ...r, type: 'review' as const })) || []),
|
||||
...(credits?.map(c => ({ ...c, type: 'credit' as const })) || []),
|
||||
...(submissions?.map(s => ({ ...s, type: 'submission' as const })) || []),
|
||||
...(rankings?.map(r => ({ ...r, type: 'ranking' as const })) || [])
|
||||
].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
||||
.slice(0, 15);
|
||||
.slice(0, 15) as ActivityEntry[];
|
||||
|
||||
setRecentActivity(combined);
|
||||
} catch (error: any) {
|
||||
|
||||
28
src/types/company.ts
Normal file
28
src/types/company.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Company-related type definitions
|
||||
*/
|
||||
|
||||
import { ImageAssignments } from '@/components/upload/EntityMultiImageUploader';
|
||||
|
||||
export interface CompanyFormData {
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
company_type: 'manufacturer' | 'designer' | 'operator' | 'property_owner';
|
||||
person_type: 'company' | 'individual' | 'firm' | 'organization';
|
||||
website_url?: string;
|
||||
founded_year?: number;
|
||||
headquarters_location?: string;
|
||||
images?: ImageAssignments;
|
||||
}
|
||||
|
||||
export interface TempCompanyData {
|
||||
name: string;
|
||||
slug: string;
|
||||
company_type: 'manufacturer' | 'designer' | 'operator' | 'property_owner';
|
||||
person_type: 'company' | 'individual' | 'firm' | 'organization';
|
||||
description?: string;
|
||||
founded_year?: number;
|
||||
headquarters_location?: string;
|
||||
website_url?: string;
|
||||
}
|
||||
@@ -260,3 +260,31 @@ export interface UserTopListItem {
|
||||
// Populated via joins
|
||||
entity?: Park | Ride | Company;
|
||||
}
|
||||
|
||||
// Extended company interface with aggregated stats
|
||||
export interface CompanyWithStats extends Company {
|
||||
ride_count?: number;
|
||||
coaster_count?: number;
|
||||
model_count?: number;
|
||||
park_count?: number;
|
||||
}
|
||||
|
||||
// Audit log entry - matches actual profile_audit_log table structure
|
||||
export interface AuditLogEntry {
|
||||
id: string;
|
||||
user_id: string;
|
||||
changed_by: string;
|
||||
action: string;
|
||||
changes: Record<string, unknown> | null;
|
||||
ip_address_hash: string;
|
||||
user_agent: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Activity entry - flexible structure for mixed activity types
|
||||
export interface ActivityEntry {
|
||||
id: string;
|
||||
type?: 'review' | 'credit' | 'submission' | 'ranking';
|
||||
created_at: string;
|
||||
[key: string]: unknown; // Allow any additional properties from different activity types
|
||||
}
|
||||
Reference in New Issue
Block a user