feat: Standardize submission display

This commit is contained in:
gpt-engineer-app[bot]
2025-10-03 15:32:23 +00:00
parent e15705e94d
commit fe33169ed7
5 changed files with 551 additions and 74 deletions

View File

@@ -0,0 +1,226 @@
import type { SubmissionItemData } from '@/types/submissions';
export interface FieldChange {
field: string;
oldValue: any;
newValue: any;
changeType: 'added' | 'removed' | 'modified';
}
export interface ImageChange {
type: 'banner' | 'card';
oldUrl?: string;
newUrl?: string;
oldId?: string;
newId?: string;
}
export interface ChangesSummary {
action: 'create' | 'edit' | 'delete';
entityType: string;
entityName?: string;
fieldChanges: FieldChange[];
imageChanges: ImageChange[];
hasLocationChange: boolean;
totalChanges: number;
}
/**
* Detects what changed between original_data and item_data
*/
export function detectChanges(item: { item_data?: any; original_data?: any; item_type: string }): ChangesSummary {
const itemData = item.item_data || {};
const originalData = item.original_data || {};
// Determine action type
const action: 'create' | 'edit' | 'delete' =
!originalData || Object.keys(originalData).length === 0 ? 'create' :
itemData.deleted ? 'delete' : 'edit';
const fieldChanges: FieldChange[] = [];
const imageChanges: ImageChange[] = [];
let hasLocationChange = false;
if (action === 'create') {
// For creates, all fields are "added"
Object.entries(itemData).forEach(([key, value]) => {
if (shouldTrackField(key) && value !== null && value !== undefined && value !== '') {
fieldChanges.push({
field: key,
oldValue: null,
newValue: value,
changeType: 'added',
});
}
});
} else if (action === 'edit') {
// Compare each field
const allKeys = new Set([
...Object.keys(itemData),
...Object.keys(originalData)
]);
allKeys.forEach(key => {
if (!shouldTrackField(key)) return;
const oldValue = originalData[key];
const newValue = itemData[key];
// Handle location changes specially
if (key === 'location' || key === 'location_id') {
if (!isEqual(oldValue, newValue)) {
hasLocationChange = true;
fieldChanges.push({
field: key,
oldValue,
newValue,
changeType: 'modified',
});
}
return;
}
// Check for changes
if (!isEqual(oldValue, newValue)) {
if ((oldValue === null || oldValue === undefined || oldValue === '') && newValue) {
fieldChanges.push({
field: key,
oldValue,
newValue,
changeType: 'added',
});
} else if ((newValue === null || newValue === undefined || newValue === '') && oldValue) {
fieldChanges.push({
field: key,
oldValue,
newValue,
changeType: 'removed',
});
} else {
fieldChanges.push({
field: key,
oldValue,
newValue,
changeType: 'modified',
});
}
}
});
// Detect image changes
detectImageChanges(originalData, itemData, imageChanges);
}
// Get entity name
const entityName = itemData.name || originalData?.name || 'Unknown';
return {
action,
entityType: item.item_type,
entityName,
fieldChanges,
imageChanges,
hasLocationChange,
totalChanges: fieldChanges.length + imageChanges.length,
};
}
/**
* Determines if a field should be tracked for changes
*/
function shouldTrackField(key: string): boolean {
const excludedFields = [
'id',
'created_at',
'updated_at',
'slug',
'image_assignments',
'banner_image_url',
'banner_image_id',
'card_image_url',
'card_image_id',
];
return !excludedFields.includes(key);
}
/**
* Deep equality check for values
*/
function isEqual(a: any, b: any): boolean {
if (a === b) return true;
if (a == null || b == null) return a === b;
if (typeof a !== typeof b) return false;
if (typeof a === 'object') {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((item, i) => isEqual(item, b[i]));
}
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(key => isEqual(a[key], b[key]));
}
return false;
}
/**
* Detects changes in banner/card images
*/
function detectImageChanges(
originalData: any,
itemData: any,
imageChanges: ImageChange[]
): void {
// Check banner image
if (originalData.banner_image_id !== itemData.banner_image_id ||
originalData.banner_image_url !== itemData.banner_image_url) {
imageChanges.push({
type: 'banner',
oldUrl: originalData.banner_image_url,
newUrl: itemData.banner_image_url,
oldId: originalData.banner_image_id,
newId: itemData.banner_image_id,
});
}
// Check card image
if (originalData.card_image_id !== itemData.card_image_id ||
originalData.card_image_url !== itemData.card_image_url) {
imageChanges.push({
type: 'card',
oldUrl: originalData.card_image_url,
newUrl: itemData.card_image_url,
oldId: originalData.card_image_id,
newId: itemData.card_image_id,
});
}
}
/**
* Format field name for display
*/
export function formatFieldName(field: string): string {
return field
.replace(/_/g, ' ')
.replace(/([A-Z])/g, ' $1')
.replace(/^./, str => str.toUpperCase())
.trim();
}
/**
* Format field value for display
*/
export function formatFieldValue(value: any): string {
if (value === null || value === undefined) return 'None';
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
if (typeof value === 'object') {
if (Array.isArray(value)) return `${value.length} items`;
return JSON.stringify(value, null, 2);
}
if (typeof value === 'number') return value.toLocaleString();
return String(value);
}