@qrush/types 2.1.32 → 2.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CSVImport.d.ts +147 -0
- package/dist/CSVImport.js +529 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV Import Types and Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides types, validation rules, and utility functions
|
|
5
|
+
* for importing events from CSV files.
|
|
6
|
+
*/
|
|
7
|
+
import { TicketType, IFireEvent, EventCategorySlug } from "./Events.js";
|
|
8
|
+
import { SupportedLanguage } from "./Common.js";
|
|
9
|
+
import musicGenres from "./Genres.js";
|
|
10
|
+
/**
|
|
11
|
+
* Represents a single row from the CSV import file.
|
|
12
|
+
* Column names are human-readable for ease of use by non-technical users.
|
|
13
|
+
*/
|
|
14
|
+
export interface CSVEventRow {
|
|
15
|
+
"Event Title": string;
|
|
16
|
+
Description: string;
|
|
17
|
+
"Start Date": string;
|
|
18
|
+
"End Date": string;
|
|
19
|
+
"Venue Name": string;
|
|
20
|
+
City: string;
|
|
21
|
+
"Image URL": string;
|
|
22
|
+
"Ticket Link"?: string;
|
|
23
|
+
"Base Price"?: string;
|
|
24
|
+
"Ticket Options"?: string;
|
|
25
|
+
"Sold Out"?: string;
|
|
26
|
+
Artists?: string;
|
|
27
|
+
Categories?: string;
|
|
28
|
+
"Main Genre"?: string;
|
|
29
|
+
"Sub Genres"?: string;
|
|
30
|
+
Tags?: string;
|
|
31
|
+
"Host Name"?: string;
|
|
32
|
+
Language?: string;
|
|
33
|
+
"Description (DE)"?: string;
|
|
34
|
+
"Description (EN)"?: string;
|
|
35
|
+
"Description (ES)"?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Maps CSV column names to internal IFireEvent field paths.
|
|
39
|
+
* Used for transforming CSV data to Firestore documents.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CSV_COLUMN_MAPPING: {
|
|
42
|
+
readonly "Event Title": "name";
|
|
43
|
+
readonly Description: "descriptionMarkdown";
|
|
44
|
+
readonly "Start Date": "eventStart";
|
|
45
|
+
readonly "End Date": "eventEnd";
|
|
46
|
+
readonly "Venue Name": "_venueNameForMatching";
|
|
47
|
+
readonly City: "_cityForMatching";
|
|
48
|
+
readonly "Image URL": "imageURI";
|
|
49
|
+
readonly "Ticket Link": "properties.ticketURL";
|
|
50
|
+
readonly "Base Price": "properties.price";
|
|
51
|
+
readonly "Ticket Options": "properties.tickets";
|
|
52
|
+
readonly "Sold Out": "properties.ticketsSoldOut";
|
|
53
|
+
readonly Artists: "artists";
|
|
54
|
+
readonly Categories: "categorySlug";
|
|
55
|
+
readonly "Main Genre": "genres.mainGenre";
|
|
56
|
+
readonly "Sub Genres": "genres.subGenres";
|
|
57
|
+
readonly Tags: "tags";
|
|
58
|
+
readonly "Host Name": "hostName";
|
|
59
|
+
readonly Language: "primaryLang";
|
|
60
|
+
readonly "Description (DE)": "descriptionTranslations.de";
|
|
61
|
+
readonly "Description (EN)": "descriptionTranslations.en";
|
|
62
|
+
readonly "Description (ES)": "descriptionTranslations.es";
|
|
63
|
+
};
|
|
64
|
+
export type CSVColumnName = keyof typeof CSV_COLUMN_MAPPING;
|
|
65
|
+
export type CSVFieldPath = (typeof CSV_COLUMN_MAPPING)[CSVColumnName];
|
|
66
|
+
export type CSVFieldType = "string" | "date" | "url" | "number" | "boolean" | "list";
|
|
67
|
+
export interface ValidationResult {
|
|
68
|
+
valid: boolean;
|
|
69
|
+
error?: string;
|
|
70
|
+
transformed?: unknown;
|
|
71
|
+
}
|
|
72
|
+
export interface CSVValidationRule {
|
|
73
|
+
required: boolean;
|
|
74
|
+
type: CSVFieldType;
|
|
75
|
+
validate?: (value: string) => ValidationResult;
|
|
76
|
+
}
|
|
77
|
+
export interface CSVRowValidationResult {
|
|
78
|
+
rowIndex: number;
|
|
79
|
+
valid: boolean;
|
|
80
|
+
errors: {
|
|
81
|
+
field: string;
|
|
82
|
+
message: string;
|
|
83
|
+
}[];
|
|
84
|
+
warnings: {
|
|
85
|
+
field: string;
|
|
86
|
+
message: string;
|
|
87
|
+
}[];
|
|
88
|
+
data?: Partial<IFireEvent>;
|
|
89
|
+
}
|
|
90
|
+
export interface CSVImportSummary {
|
|
91
|
+
totalRows: number;
|
|
92
|
+
validRows: number;
|
|
93
|
+
invalidRows: number;
|
|
94
|
+
warningRows: number;
|
|
95
|
+
results: CSVRowValidationResult[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validation rules for each CSV column.
|
|
99
|
+
* Defines required/optional status, type, and custom validation functions.
|
|
100
|
+
*/
|
|
101
|
+
export declare const CSV_VALIDATION_RULES: Record<keyof CSVEventRow, CSVValidationRule>;
|
|
102
|
+
/**
|
|
103
|
+
* Parses a date string in EU format (DD.MM.YYYY HH:mm) or ISO 8601.
|
|
104
|
+
* @param dateStr The date string to parse
|
|
105
|
+
* @returns A Date object or null if parsing fails
|
|
106
|
+
*/
|
|
107
|
+
export declare function parseEUDate(dateStr: string): Date | null;
|
|
108
|
+
/**
|
|
109
|
+
* Parses ticket options string in format "Name:Price;Name:Price"
|
|
110
|
+
* @param optionsStr The ticket options string
|
|
111
|
+
* @returns Array of TicketType objects
|
|
112
|
+
*/
|
|
113
|
+
export declare function parseTicketOptions(optionsStr: string): TicketType[];
|
|
114
|
+
/**
|
|
115
|
+
* Parses a comma-separated list into an array of trimmed strings
|
|
116
|
+
* @param str The comma-separated string
|
|
117
|
+
* @returns Array of trimmed strings
|
|
118
|
+
*/
|
|
119
|
+
export declare function parseCommaSeparatedList(str: string): string[];
|
|
120
|
+
/**
|
|
121
|
+
* Validates if a string is a valid event category slug
|
|
122
|
+
*/
|
|
123
|
+
export declare function isValidEventCategorySlug(slug: string): slug is EventCategorySlug;
|
|
124
|
+
/**
|
|
125
|
+
* Validates if a string is a valid music genre
|
|
126
|
+
*/
|
|
127
|
+
export declare function isValidGenre(genre: string): genre is keyof typeof musicGenres;
|
|
128
|
+
/**
|
|
129
|
+
* Validates if a string is a valid supported language
|
|
130
|
+
*/
|
|
131
|
+
export declare function isValidLanguage(lang: string): lang is SupportedLanguage;
|
|
132
|
+
/**
|
|
133
|
+
* Validates a single CSV row and returns validation result
|
|
134
|
+
*/
|
|
135
|
+
export declare function validateCSVRow(row: Partial<CSVEventRow>, rowIndex: number): CSVRowValidationResult;
|
|
136
|
+
/**
|
|
137
|
+
* Returns the required CSV headers for import
|
|
138
|
+
*/
|
|
139
|
+
export declare function getCSVHeaders(): string[];
|
|
140
|
+
/**
|
|
141
|
+
* Returns only the required CSV headers
|
|
142
|
+
*/
|
|
143
|
+
export declare function getRequiredCSVHeaders(): string[];
|
|
144
|
+
/**
|
|
145
|
+
* Sample CSV template row with example data
|
|
146
|
+
*/
|
|
147
|
+
export declare const CSV_TEMPLATE_EXAMPLE: CSVEventRow;
|
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSV Import Types and Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides types, validation rules, and utility functions
|
|
5
|
+
* for importing events from CSV files.
|
|
6
|
+
*/
|
|
7
|
+
import { EVENT_CATEGORY_SLUGS } from "./Common.js";
|
|
8
|
+
import musicGenres from "./Genres.js";
|
|
9
|
+
// ==========================================
|
|
10
|
+
// B. CSV_COLUMN_MAPPING Constant
|
|
11
|
+
// ==========================================
|
|
12
|
+
/**
|
|
13
|
+
* Maps CSV column names to internal IFireEvent field paths.
|
|
14
|
+
* Used for transforming CSV data to Firestore documents.
|
|
15
|
+
*/
|
|
16
|
+
export const CSV_COLUMN_MAPPING = {
|
|
17
|
+
"Event Title": "name",
|
|
18
|
+
Description: "descriptionMarkdown",
|
|
19
|
+
"Start Date": "eventStart",
|
|
20
|
+
"End Date": "eventEnd",
|
|
21
|
+
"Venue Name": "_venueNameForMatching", // Internal use for location matching
|
|
22
|
+
City: "_cityForMatching", // Internal use for location matching
|
|
23
|
+
"Image URL": "imageURI",
|
|
24
|
+
"Ticket Link": "properties.ticketURL",
|
|
25
|
+
"Base Price": "properties.price",
|
|
26
|
+
"Ticket Options": "properties.tickets",
|
|
27
|
+
"Sold Out": "properties.ticketsSoldOut",
|
|
28
|
+
Artists: "artists",
|
|
29
|
+
Categories: "categorySlug",
|
|
30
|
+
"Main Genre": "genres.mainGenre",
|
|
31
|
+
"Sub Genres": "genres.subGenres",
|
|
32
|
+
Tags: "tags",
|
|
33
|
+
"Host Name": "hostName",
|
|
34
|
+
Language: "primaryLang",
|
|
35
|
+
"Description (DE)": "descriptionTranslations.de",
|
|
36
|
+
"Description (EN)": "descriptionTranslations.en",
|
|
37
|
+
"Description (ES)": "descriptionTranslations.es",
|
|
38
|
+
};
|
|
39
|
+
// ==========================================
|
|
40
|
+
// D. CSV_VALIDATION_RULES Constant
|
|
41
|
+
// ==========================================
|
|
42
|
+
/**
|
|
43
|
+
* Validation rules for each CSV column.
|
|
44
|
+
* Defines required/optional status, type, and custom validation functions.
|
|
45
|
+
*/
|
|
46
|
+
export const CSV_VALIDATION_RULES = {
|
|
47
|
+
"Event Title": {
|
|
48
|
+
required: true,
|
|
49
|
+
type: "string",
|
|
50
|
+
validate: (value) => {
|
|
51
|
+
if (!value || value.trim().length === 0) {
|
|
52
|
+
return { valid: false, error: "Event title is required" };
|
|
53
|
+
}
|
|
54
|
+
if (value.length > 200) {
|
|
55
|
+
return { valid: false, error: "Event title must be 200 characters or less" };
|
|
56
|
+
}
|
|
57
|
+
return { valid: true, transformed: value.trim() };
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
Description: {
|
|
61
|
+
required: true,
|
|
62
|
+
type: "string",
|
|
63
|
+
validate: (value) => {
|
|
64
|
+
if (!value || value.trim().length === 0) {
|
|
65
|
+
return { valid: false, error: "Description is required" };
|
|
66
|
+
}
|
|
67
|
+
return { valid: true, transformed: value.trim() };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
"Start Date": {
|
|
71
|
+
required: true,
|
|
72
|
+
type: "date",
|
|
73
|
+
validate: (value) => {
|
|
74
|
+
const parsed = parseEUDate(value);
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
return {
|
|
77
|
+
valid: false,
|
|
78
|
+
error: "Invalid date format. Use DD.MM.YYYY HH:mm or ISO 8601",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { valid: true, transformed: parsed };
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
"End Date": {
|
|
85
|
+
required: true,
|
|
86
|
+
type: "date",
|
|
87
|
+
validate: (value) => {
|
|
88
|
+
const parsed = parseEUDate(value);
|
|
89
|
+
if (!parsed) {
|
|
90
|
+
return {
|
|
91
|
+
valid: false,
|
|
92
|
+
error: "Invalid date format. Use DD.MM.YYYY HH:mm or ISO 8601",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { valid: true, transformed: parsed };
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
"Venue Name": {
|
|
99
|
+
required: true,
|
|
100
|
+
type: "string",
|
|
101
|
+
validate: (value) => {
|
|
102
|
+
if (!value || value.trim().length === 0) {
|
|
103
|
+
return { valid: false, error: "Venue name is required" };
|
|
104
|
+
}
|
|
105
|
+
return { valid: true, transformed: value.trim() };
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
City: {
|
|
109
|
+
required: true,
|
|
110
|
+
type: "string",
|
|
111
|
+
validate: (value) => {
|
|
112
|
+
if (!value || value.trim().length === 0) {
|
|
113
|
+
return { valid: false, error: "City is required" };
|
|
114
|
+
}
|
|
115
|
+
return { valid: true, transformed: value.trim() };
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
"Image URL": {
|
|
119
|
+
required: true,
|
|
120
|
+
type: "url",
|
|
121
|
+
validate: (value) => {
|
|
122
|
+
if (!value || value.trim().length === 0) {
|
|
123
|
+
return { valid: false, error: "Image URL is required" };
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
new URL(value);
|
|
127
|
+
return { valid: true, transformed: value.trim() };
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { valid: false, error: "Invalid URL format" };
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
"Ticket Link": {
|
|
135
|
+
required: false,
|
|
136
|
+
type: "url",
|
|
137
|
+
validate: (value) => {
|
|
138
|
+
if (!value || value.trim().length === 0) {
|
|
139
|
+
return { valid: true, transformed: undefined };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
new URL(value);
|
|
143
|
+
return { valid: true, transformed: value.trim() };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { valid: false, error: "Invalid URL format" };
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
"Base Price": {
|
|
151
|
+
required: false,
|
|
152
|
+
type: "number",
|
|
153
|
+
validate: (value) => {
|
|
154
|
+
if (!value || value.trim().length === 0) {
|
|
155
|
+
return { valid: true, transformed: undefined };
|
|
156
|
+
}
|
|
157
|
+
const num = parseFloat(value.replace(",", "."));
|
|
158
|
+
if (isNaN(num) || num < 0) {
|
|
159
|
+
return { valid: false, error: "Price must be a positive number" };
|
|
160
|
+
}
|
|
161
|
+
return { valid: true, transformed: num };
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
"Ticket Options": {
|
|
165
|
+
required: false,
|
|
166
|
+
type: "string",
|
|
167
|
+
validate: (value) => {
|
|
168
|
+
if (!value || value.trim().length === 0) {
|
|
169
|
+
return { valid: true, transformed: undefined };
|
|
170
|
+
}
|
|
171
|
+
const tickets = parseTicketOptions(value);
|
|
172
|
+
if (tickets.length === 0) {
|
|
173
|
+
return {
|
|
174
|
+
valid: false,
|
|
175
|
+
error: "Invalid ticket format. Use Name:Price;Name:Price",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { valid: true, transformed: tickets };
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
"Sold Out": {
|
|
182
|
+
required: false,
|
|
183
|
+
type: "boolean",
|
|
184
|
+
validate: (value) => {
|
|
185
|
+
if (!value || value.trim().length === 0) {
|
|
186
|
+
return { valid: true, transformed: undefined };
|
|
187
|
+
}
|
|
188
|
+
const lower = value.toLowerCase().trim();
|
|
189
|
+
if (lower === "true" || lower === "yes" || lower === "1") {
|
|
190
|
+
return { valid: true, transformed: true };
|
|
191
|
+
}
|
|
192
|
+
if (lower === "false" || lower === "no" || lower === "0") {
|
|
193
|
+
return { valid: true, transformed: false };
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
valid: false,
|
|
197
|
+
error: 'Invalid boolean. Use true/false, yes/no, or 1/0',
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
Artists: {
|
|
202
|
+
required: false,
|
|
203
|
+
type: "list",
|
|
204
|
+
validate: (value) => {
|
|
205
|
+
if (!value || value.trim().length === 0) {
|
|
206
|
+
return { valid: true, transformed: [] };
|
|
207
|
+
}
|
|
208
|
+
const artists = parseCommaSeparatedList(value);
|
|
209
|
+
return { valid: true, transformed: artists };
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
Categories: {
|
|
213
|
+
required: false,
|
|
214
|
+
type: "list",
|
|
215
|
+
validate: (value) => {
|
|
216
|
+
if (!value || value.trim().length === 0) {
|
|
217
|
+
return { valid: true, transformed: [] };
|
|
218
|
+
}
|
|
219
|
+
const categories = parseCommaSeparatedList(value);
|
|
220
|
+
// Validate against known category slugs
|
|
221
|
+
const validCategories = categories.filter((cat) => isValidEventCategorySlug(cat));
|
|
222
|
+
const invalidCategories = categories.filter((cat) => !isValidEventCategorySlug(cat));
|
|
223
|
+
if (invalidCategories.length > 0) {
|
|
224
|
+
return {
|
|
225
|
+
valid: false,
|
|
226
|
+
error: `Invalid categories: ${invalidCategories.join(", ")}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return { valid: true, transformed: validCategories };
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
"Main Genre": {
|
|
233
|
+
required: false,
|
|
234
|
+
type: "string",
|
|
235
|
+
validate: (value) => {
|
|
236
|
+
if (!value || value.trim().length === 0) {
|
|
237
|
+
return { valid: true, transformed: undefined };
|
|
238
|
+
}
|
|
239
|
+
const genre = value.trim().toLowerCase();
|
|
240
|
+
if (!isValidGenre(genre)) {
|
|
241
|
+
return {
|
|
242
|
+
valid: false,
|
|
243
|
+
error: `Invalid genre: ${genre}. Check allowed genres.`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { valid: true, transformed: genre };
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
"Sub Genres": {
|
|
250
|
+
required: false,
|
|
251
|
+
type: "list",
|
|
252
|
+
validate: (value) => {
|
|
253
|
+
if (!value || value.trim().length === 0) {
|
|
254
|
+
return { valid: true, transformed: [] };
|
|
255
|
+
}
|
|
256
|
+
const genres = parseCommaSeparatedList(value);
|
|
257
|
+
return { valid: true, transformed: genres };
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
Tags: {
|
|
261
|
+
required: false,
|
|
262
|
+
type: "list",
|
|
263
|
+
validate: (value) => {
|
|
264
|
+
if (!value || value.trim().length === 0) {
|
|
265
|
+
return { valid: true, transformed: [] };
|
|
266
|
+
}
|
|
267
|
+
return { valid: true, transformed: parseCommaSeparatedList(value) };
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
"Host Name": {
|
|
271
|
+
required: false,
|
|
272
|
+
type: "string",
|
|
273
|
+
validate: (value) => {
|
|
274
|
+
if (!value || value.trim().length === 0) {
|
|
275
|
+
return { valid: true, transformed: undefined };
|
|
276
|
+
}
|
|
277
|
+
return { valid: true, transformed: value.trim() };
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
Language: {
|
|
281
|
+
required: false,
|
|
282
|
+
type: "string",
|
|
283
|
+
validate: (value) => {
|
|
284
|
+
if (!value || value.trim().length === 0) {
|
|
285
|
+
return { valid: true, transformed: undefined };
|
|
286
|
+
}
|
|
287
|
+
const lang = value.toLowerCase().trim();
|
|
288
|
+
if (!isValidLanguage(lang)) {
|
|
289
|
+
return {
|
|
290
|
+
valid: false,
|
|
291
|
+
error: `Invalid language: ${lang}. Use de, en, or es`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return { valid: true, transformed: lang };
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
"Description (DE)": {
|
|
298
|
+
required: false,
|
|
299
|
+
type: "string",
|
|
300
|
+
validate: (value) => {
|
|
301
|
+
if (!value || value.trim().length === 0) {
|
|
302
|
+
return { valid: true, transformed: undefined };
|
|
303
|
+
}
|
|
304
|
+
return { valid: true, transformed: value.trim() };
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
"Description (EN)": {
|
|
308
|
+
required: false,
|
|
309
|
+
type: "string",
|
|
310
|
+
validate: (value) => {
|
|
311
|
+
if (!value || value.trim().length === 0) {
|
|
312
|
+
return { valid: true, transformed: undefined };
|
|
313
|
+
}
|
|
314
|
+
return { valid: true, transformed: value.trim() };
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
"Description (ES)": {
|
|
318
|
+
required: false,
|
|
319
|
+
type: "string",
|
|
320
|
+
validate: (value) => {
|
|
321
|
+
if (!value || value.trim().length === 0) {
|
|
322
|
+
return { valid: true, transformed: undefined };
|
|
323
|
+
}
|
|
324
|
+
return { valid: true, transformed: value.trim() };
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
// ==========================================
|
|
329
|
+
// E. Parsing Utilities
|
|
330
|
+
// ==========================================
|
|
331
|
+
/**
|
|
332
|
+
* Parses a date string in EU format (DD.MM.YYYY HH:mm) or ISO 8601.
|
|
333
|
+
* @param dateStr The date string to parse
|
|
334
|
+
* @returns A Date object or null if parsing fails
|
|
335
|
+
*/
|
|
336
|
+
export function parseEUDate(dateStr) {
|
|
337
|
+
if (!dateStr || typeof dateStr !== "string") {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const trimmed = dateStr.trim();
|
|
341
|
+
// Try ISO 8601 first
|
|
342
|
+
const isoDate = new Date(trimmed);
|
|
343
|
+
if (!isNaN(isoDate.getTime()) && trimmed.includes("-")) {
|
|
344
|
+
return isoDate;
|
|
345
|
+
}
|
|
346
|
+
// Try EU format: DD.MM.YYYY HH:mm or DD.MM.YYYY
|
|
347
|
+
const euPattern = /^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2}))?$/;
|
|
348
|
+
const match = trimmed.match(euPattern);
|
|
349
|
+
if (!match) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const day = parseInt(match[1], 10);
|
|
353
|
+
const month = parseInt(match[2], 10) - 1; // JavaScript months are 0-indexed
|
|
354
|
+
const year = parseInt(match[3], 10);
|
|
355
|
+
const hour = match[4] ? parseInt(match[4], 10) : 0;
|
|
356
|
+
const minute = match[5] ? parseInt(match[5], 10) : 0;
|
|
357
|
+
// Validate ranges
|
|
358
|
+
if (month < 0 ||
|
|
359
|
+
month > 11 ||
|
|
360
|
+
day < 1 ||
|
|
361
|
+
day > 31 ||
|
|
362
|
+
hour < 0 ||
|
|
363
|
+
hour > 23 ||
|
|
364
|
+
minute < 0 ||
|
|
365
|
+
minute > 59) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const date = new Date(year, month, day, hour, minute);
|
|
369
|
+
// Check if the date is valid (e.g., catches Feb 30)
|
|
370
|
+
if (date.getFullYear() !== year ||
|
|
371
|
+
date.getMonth() !== month ||
|
|
372
|
+
date.getDate() !== day) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return date;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Parses ticket options string in format "Name:Price;Name:Price"
|
|
379
|
+
* @param optionsStr The ticket options string
|
|
380
|
+
* @returns Array of TicketType objects
|
|
381
|
+
*/
|
|
382
|
+
export function parseTicketOptions(optionsStr) {
|
|
383
|
+
if (!optionsStr || typeof optionsStr !== "string") {
|
|
384
|
+
return [];
|
|
385
|
+
}
|
|
386
|
+
const tickets = [];
|
|
387
|
+
const options = optionsStr.split(";").map((s) => s.trim());
|
|
388
|
+
for (const option of options) {
|
|
389
|
+
if (!option)
|
|
390
|
+
continue;
|
|
391
|
+
const parts = option.split(":").map((s) => s.trim());
|
|
392
|
+
if (parts.length !== 2)
|
|
393
|
+
continue;
|
|
394
|
+
const [name, priceStr] = parts;
|
|
395
|
+
const price = parseFloat(priceStr.replace(",", "."));
|
|
396
|
+
if (!name || isNaN(price) || price < 0)
|
|
397
|
+
continue;
|
|
398
|
+
tickets.push({
|
|
399
|
+
id: generateTicketId(),
|
|
400
|
+
name,
|
|
401
|
+
price,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return tickets;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Parses a comma-separated list into an array of trimmed strings
|
|
408
|
+
* @param str The comma-separated string
|
|
409
|
+
* @returns Array of trimmed strings
|
|
410
|
+
*/
|
|
411
|
+
export function parseCommaSeparatedList(str) {
|
|
412
|
+
if (!str || typeof str !== "string") {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
return str
|
|
416
|
+
.split(",")
|
|
417
|
+
.map((s) => s.trim())
|
|
418
|
+
.filter((s) => s.length > 0);
|
|
419
|
+
}
|
|
420
|
+
// ==========================================
|
|
421
|
+
// F. Helper Functions
|
|
422
|
+
// ==========================================
|
|
423
|
+
/**
|
|
424
|
+
* Generates a unique ID for ticket types
|
|
425
|
+
*/
|
|
426
|
+
function generateTicketId() {
|
|
427
|
+
return `ticket_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Validates if a string is a valid event category slug
|
|
431
|
+
*/
|
|
432
|
+
export function isValidEventCategorySlug(slug) {
|
|
433
|
+
return EVENT_CATEGORY_SLUGS.includes(slug.toLowerCase());
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Validates if a string is a valid music genre
|
|
437
|
+
*/
|
|
438
|
+
export function isValidGenre(genre) {
|
|
439
|
+
return genre in musicGenres;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Validates if a string is a valid supported language
|
|
443
|
+
*/
|
|
444
|
+
export function isValidLanguage(lang) {
|
|
445
|
+
return ["de", "en", "es"].includes(lang);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Validates a single CSV row and returns validation result
|
|
449
|
+
*/
|
|
450
|
+
export function validateCSVRow(row, rowIndex) {
|
|
451
|
+
const errors = [];
|
|
452
|
+
const warnings = [];
|
|
453
|
+
const data = {};
|
|
454
|
+
for (const [field, rule] of Object.entries(CSV_VALIDATION_RULES)) {
|
|
455
|
+
const columnName = field;
|
|
456
|
+
const value = row[columnName] ?? "";
|
|
457
|
+
// Check required fields
|
|
458
|
+
if (rule.required && (!value || String(value).trim().length === 0)) {
|
|
459
|
+
errors.push({ field: columnName, message: `${columnName} is required` });
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
// Run custom validation if provided
|
|
463
|
+
if (rule.validate && value) {
|
|
464
|
+
const result = rule.validate(String(value));
|
|
465
|
+
if (!result.valid) {
|
|
466
|
+
if (rule.required) {
|
|
467
|
+
errors.push({ field: columnName, message: result.error ?? "Validation failed" });
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
warnings.push({ field: columnName, message: result.error ?? "Validation failed" });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
else if (result.transformed !== undefined) {
|
|
474
|
+
const fieldPath = CSV_COLUMN_MAPPING[columnName];
|
|
475
|
+
data[fieldPath] = result.transformed;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
rowIndex,
|
|
481
|
+
valid: errors.length === 0,
|
|
482
|
+
errors,
|
|
483
|
+
warnings,
|
|
484
|
+
data: errors.length === 0 ? data : undefined,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// ==========================================
|
|
488
|
+
// G. CSV Template
|
|
489
|
+
// ==========================================
|
|
490
|
+
/**
|
|
491
|
+
* Returns the required CSV headers for import
|
|
492
|
+
*/
|
|
493
|
+
export function getCSVHeaders() {
|
|
494
|
+
return Object.keys(CSV_VALIDATION_RULES);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Returns only the required CSV headers
|
|
498
|
+
*/
|
|
499
|
+
export function getRequiredCSVHeaders() {
|
|
500
|
+
return Object.entries(CSV_VALIDATION_RULES)
|
|
501
|
+
.filter(([, rule]) => rule.required)
|
|
502
|
+
.map(([field]) => field);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Sample CSV template row with example data
|
|
506
|
+
*/
|
|
507
|
+
export const CSV_TEMPLATE_EXAMPLE = {
|
|
508
|
+
"Event Title": "Summer Night Party",
|
|
509
|
+
Description: "Join us for an unforgettable summer night!",
|
|
510
|
+
"Start Date": "14.06.2025 22:00",
|
|
511
|
+
"End Date": "15.06.2025 06:00",
|
|
512
|
+
"Venue Name": "Club Matrix",
|
|
513
|
+
City: "Berlin",
|
|
514
|
+
"Image URL": "https://example.com/event-image.jpg",
|
|
515
|
+
"Ticket Link": "https://example.com/tickets",
|
|
516
|
+
"Base Price": "15.00",
|
|
517
|
+
"Ticket Options": "Early Bird:12.00;Regular:15.00;VIP:30.00",
|
|
518
|
+
"Sold Out": "false",
|
|
519
|
+
Artists: "DJ Shadow, Bonobo, Four Tet",
|
|
520
|
+
Categories: "clubnight,party",
|
|
521
|
+
"Main Genre": "techno",
|
|
522
|
+
"Sub Genres": "house,minimal",
|
|
523
|
+
Tags: "summer,nightlife,electronic",
|
|
524
|
+
"Host Name": "Matrix Events",
|
|
525
|
+
Language: "de",
|
|
526
|
+
"Description (DE)": "Feiert mit uns eine unvergessliche Sommernacht!",
|
|
527
|
+
"Description (EN)": "Join us for an unforgettable summer night!",
|
|
528
|
+
"Description (ES)": "Celebra con nosotros una noche de verano inolvidable!",
|
|
529
|
+
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED