Skip to content

Commit e61ba3e

Browse files
committed
feat(sets): manage conflicts on import
1 parent f4b7221 commit e61ba3e

27 files changed

+1833
-477
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ src/
113113
- **Component Extraction**: When a section of JSX + logic becomes substantial (>30 lines) or reusable, extract it into a separate component. Place in appropriate directory: page-specific components in `components/PageName/`, reusable ones in `components/`
114114
- **Forms**: ALL forms must use react-hook-form with proper validation. Never use plain HTML forms or manual state management for form inputs. Use @hookform/resolvers for validation schemas when needed.
115115
- **Long Components**: Break long components (>150 lines) into smaller focused pieces. Follow the FilterSortControls pattern of primary controls + expandable sections.
116+
- No barrel exports - always import directly from file path
117+
- Use `cn` for conditional class names instead of template literals
116118

117119
#### Function Definitions After Return in React Components
118120

src/hooks/queries/artists/useDuplicateArtists.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ async function fetchDuplicateArtists(): Promise<DuplicateGroup[]> {
2727

2828
const nameCounts = duplicateNames.reduce(
2929
(acc, artist) => {
30-
acc[artist.name] = (acc[artist.name] || 0) + 1;
30+
acc[artist.name.toLocaleLowerCase()] =
31+
(acc[artist.name.toLocaleLowerCase()] || 0) + 1;
3132
return acc;
3233
},
3334
{} as Record<string, number>,
@@ -52,7 +53,7 @@ async function fetchDuplicateArtists(): Promise<DuplicateGroup[]> {
5253
artist_music_genres (music_genre_id)
5354
`,
5455
)
55-
.eq("name", name)
56+
.ilike("name", name)
5657
.eq("archived", false)
5758
.order("created_at", { ascending: false });
5859

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useState, useCallback } from "react";
2+
import type {
3+
ImportConflict,
4+
ConflictResolution,
5+
} from "@/services/csv/conflictDetector";
6+
7+
interface UseImportConflictResolutionProps {
8+
conflicts: ImportConflict[];
9+
onResolutionsChange?: (resolutions: Map<number, ConflictResolution>) => void;
10+
}
11+
12+
export function useImportConflictResolution({
13+
conflicts,
14+
onResolutionsChange,
15+
}: UseImportConflictResolutionProps) {
16+
const [resolutions, setResolutions] = useState<
17+
Map<number, ConflictResolution>
18+
>(new Map());
19+
const [activeConflictIndex, setActiveConflictIndex] = useState<number | null>(
20+
null,
21+
);
22+
23+
const updateResolution = useCallback(
24+
(conflictIndex: number, resolution: ConflictResolution) => {
25+
setResolutions((prev) => {
26+
const newResolutions = new Map(prev);
27+
newResolutions.set(conflictIndex, resolution);
28+
onResolutionsChange?.(newResolutions);
29+
return newResolutions;
30+
});
31+
},
32+
[onResolutionsChange],
33+
);
34+
35+
const applyBulkResolution = useCallback(
36+
(resolution: ConflictResolution) => {
37+
setResolutions((prev) => {
38+
const newResolutions = new Map(prev);
39+
conflicts.forEach((_, index) => {
40+
if (!newResolutions.has(index)) {
41+
newResolutions.set(index, resolution);
42+
}
43+
});
44+
onResolutionsChange?.(newResolutions);
45+
return newResolutions;
46+
});
47+
},
48+
[conflicts, onResolutionsChange],
49+
);
50+
51+
const getUnresolvedCount = useCallback(() => {
52+
return conflicts.length - resolutions.size;
53+
}, [conflicts.length, resolutions.size]);
54+
55+
const getResolutionSummary = useCallback(() => {
56+
const summary = {
57+
skip: 0,
58+
import_new: 0,
59+
merge: 0,
60+
};
61+
62+
resolutions.forEach((resolution) => {
63+
summary[resolution.type]++;
64+
});
65+
66+
return summary;
67+
}, [resolutions]);
68+
69+
const openConflictComparison = useCallback(
70+
(conflictIndex: number, _targetArtistId?: string) => {
71+
setActiveConflictIndex(conflictIndex);
72+
},
73+
[],
74+
);
75+
76+
const closeConflictComparison = useCallback(() => {
77+
setActiveConflictIndex(null);
78+
}, []);
79+
80+
const getActiveConflict = useCallback(() => {
81+
if (activeConflictIndex === null) return null;
82+
return conflicts[activeConflictIndex] || null;
83+
}, [conflicts, activeConflictIndex]);
84+
85+
return {
86+
resolutions,
87+
activeConflictIndex,
88+
updateResolution,
89+
applyBulkResolution,
90+
getUnresolvedCount,
91+
getResolutionSummary,
92+
openConflictComparison,
93+
closeConflictComparison,
94+
getActiveConflict,
95+
};
96+
}

src/lib/timeUtils.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,19 @@ export function toDatetimeLocal(isoString: string | null): string {
108108

109109
// Helper function to convert local datetime-local to UTC ISO string
110110
export function toISOString(datetimeLocal: string): string {
111-
if (!datetimeLocal) return "";
112-
const localDate = new Date(datetimeLocal);
113-
const userTimeZone = getUserTimeZone();
114-
const utcDate = fromZonedTime(localDate, userTimeZone);
111+
return convertLocalTimeToUTC(datetimeLocal, getUserTimeZone()) || "";
112+
}
113+
114+
// Helper function to convert local time string to UTC for database storage (used in CSV imports)
115+
export function convertLocalTimeToUTC(
116+
timeString: string | undefined,
117+
timezone: string,
118+
): string | null {
119+
if (!timeString) return null;
120+
121+
// Parse the time string and interpret it as being in the specified timezone
122+
// First create a date object assuming the time is in the target timezone
123+
const utcDate = fromZonedTime(timeString, timezone);
124+
115125
return utcDate.toISOString();
116126
}

0 commit comments

Comments
 (0)