@aws505/sheetsite 1.0.0
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/README.md +105 -0
- package/dist/components/index.js +1696 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +1630 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/config/index.js +1840 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +1793 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/data/index.js +1296 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/index.mjs +1220 -0
- package/dist/data/index.mjs.map +1 -0
- package/dist/index.js +5433 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5285 -0
- package/dist/index.mjs.map +1 -0
- package/dist/seo/index.js +187 -0
- package/dist/seo/index.js.map +1 -0
- package/dist/seo/index.mjs +155 -0
- package/dist/seo/index.mjs.map +1 -0
- package/dist/theme/index.js +552 -0
- package/dist/theme/index.js.map +1 -0
- package/dist/theme/index.mjs +526 -0
- package/dist/theme/index.mjs.map +1 -0
- package/package.json +96 -0
- package/src/components/index.ts +41 -0
- package/src/components/layout/Footer.tsx +234 -0
- package/src/components/layout/Header.tsx +134 -0
- package/src/components/sections/FAQ.tsx +178 -0
- package/src/components/sections/Gallery.tsx +107 -0
- package/src/components/sections/Hero.tsx +202 -0
- package/src/components/sections/Hours.tsx +225 -0
- package/src/components/sections/Services.tsx +216 -0
- package/src/components/sections/Testimonials.tsx +184 -0
- package/src/components/ui/Button.tsx +158 -0
- package/src/components/ui/Card.tsx +162 -0
- package/src/components/ui/Icons.tsx +508 -0
- package/src/config/index.ts +207 -0
- package/src/config/presets/generic.ts +153 -0
- package/src/config/presets/home-kitchen.ts +154 -0
- package/src/config/presets/index.ts +708 -0
- package/src/config/presets/professional.ts +165 -0
- package/src/config/presets/repair.ts +160 -0
- package/src/config/presets/restaurant.ts +162 -0
- package/src/config/presets/salon.ts +178 -0
- package/src/config/presets/tailor.ts +159 -0
- package/src/config/types.ts +314 -0
- package/src/data/csv-parser.ts +154 -0
- package/src/data/defaults.ts +202 -0
- package/src/data/google-drive.ts +148 -0
- package/src/data/index.ts +535 -0
- package/src/data/sheets.ts +709 -0
- package/src/data/types.ts +379 -0
- package/src/seo/index.ts +272 -0
- package/src/theme/colors.ts +351 -0
- package/src/theme/index.ts +249 -0
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SheetSite Data Module
|
|
3
|
+
*
|
|
4
|
+
* Provides all data-related functionality:
|
|
5
|
+
* - Google Sheets integration (public CSV and private API modes)
|
|
6
|
+
* - Type definitions for site data
|
|
7
|
+
* - CSV parsing utilities
|
|
8
|
+
* - Google Drive URL utilities
|
|
9
|
+
* - Default data and fallbacks
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Types
|
|
13
|
+
export * from './types';
|
|
14
|
+
|
|
15
|
+
// Google Sheets integration
|
|
16
|
+
export {
|
|
17
|
+
fetchSheetData,
|
|
18
|
+
fetchPublicSheetData,
|
|
19
|
+
fetchPrivateSheetData,
|
|
20
|
+
fetchInlineData,
|
|
21
|
+
mergeSiteData,
|
|
22
|
+
type FetchOptions,
|
|
23
|
+
} from './sheets';
|
|
24
|
+
|
|
25
|
+
// CSV parsing
|
|
26
|
+
export {
|
|
27
|
+
parseCSV,
|
|
28
|
+
normalizeHeader,
|
|
29
|
+
mapRows,
|
|
30
|
+
parseBoolean,
|
|
31
|
+
parseNumber,
|
|
32
|
+
parseInteger,
|
|
33
|
+
parseSortOrder,
|
|
34
|
+
type ParsedRow,
|
|
35
|
+
} from './csv-parser';
|
|
36
|
+
|
|
37
|
+
// Google Drive utilities
|
|
38
|
+
export {
|
|
39
|
+
normalizeGoogleDriveUrl,
|
|
40
|
+
getGoogleDriveThumbnail,
|
|
41
|
+
isGoogleDriveUrl,
|
|
42
|
+
extractGoogleDriveFileId,
|
|
43
|
+
processImageUrls,
|
|
44
|
+
processImageUrlsInArray,
|
|
45
|
+
} from './google-drive';
|
|
46
|
+
|
|
47
|
+
// Defaults
|
|
48
|
+
export {
|
|
49
|
+
DEFAULT_SITE_DATA,
|
|
50
|
+
createDefaultSiteData,
|
|
51
|
+
industryDefaults,
|
|
52
|
+
} from './defaults';
|
|
53
|
+
|
|
54
|
+
// =============================================================================
|
|
55
|
+
// CONVENIENCE FUNCTIONS
|
|
56
|
+
// =============================================================================
|
|
57
|
+
|
|
58
|
+
import type { SheetConfig, SiteData, PartialSiteData } from './types';
|
|
59
|
+
import { fetchSheetData, mergeSiteData } from './sheets';
|
|
60
|
+
import { DEFAULT_SITE_DATA, createDefaultSiteData, industryDefaults } from './defaults';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* In-memory cache for site data.
|
|
64
|
+
*/
|
|
65
|
+
let cachedSiteData: SiteData | null = null;
|
|
66
|
+
let cacheTimestamp: number = 0;
|
|
67
|
+
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clear the in-memory cache.
|
|
71
|
+
* Call this when you need to force a refresh.
|
|
72
|
+
*/
|
|
73
|
+
export function clearSiteDataCache(): void {
|
|
74
|
+
cachedSiteData = null;
|
|
75
|
+
cacheTimestamp = 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get site data with caching.
|
|
80
|
+
* This is the primary function for fetching site data in your application.
|
|
81
|
+
*
|
|
82
|
+
* @param config - Sheet configuration (public CSV or private API)
|
|
83
|
+
* @param defaults - Default data to merge with (optional)
|
|
84
|
+
* @returns Complete site data
|
|
85
|
+
*/
|
|
86
|
+
export async function getSiteData(
|
|
87
|
+
config: SheetConfig | undefined,
|
|
88
|
+
defaults: SiteData = DEFAULT_SITE_DATA
|
|
89
|
+
): Promise<SiteData> {
|
|
90
|
+
// Check cache
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
if (cachedSiteData && now - cacheTimestamp < CACHE_DURATION) {
|
|
93
|
+
return cachedSiteData;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If no config, return defaults
|
|
97
|
+
if (!config) {
|
|
98
|
+
return defaults;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const partialData = await fetchSheetData(config);
|
|
103
|
+
const siteData = mergeSiteData(partialData, defaults);
|
|
104
|
+
|
|
105
|
+
// Update cache
|
|
106
|
+
cachedSiteData = siteData;
|
|
107
|
+
cacheTimestamp = now;
|
|
108
|
+
|
|
109
|
+
return siteData;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Error fetching site data:', error);
|
|
112
|
+
return defaults;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get site data with a custom default based on business type.
|
|
118
|
+
*
|
|
119
|
+
* @param config - Sheet configuration
|
|
120
|
+
* @param businessName - Name of the business
|
|
121
|
+
* @returns Complete site data
|
|
122
|
+
*/
|
|
123
|
+
export async function getSiteDataWithName(
|
|
124
|
+
config: SheetConfig | undefined,
|
|
125
|
+
businessName: string
|
|
126
|
+
): Promise<SiteData> {
|
|
127
|
+
const defaults = createDefaultSiteData(businessName);
|
|
128
|
+
return getSiteData(config, defaults);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a sheet configuration from environment variables.
|
|
133
|
+
* This is a convenience function for common setup patterns.
|
|
134
|
+
*
|
|
135
|
+
* Expected environment variables for public CSV mode:
|
|
136
|
+
* - SHEET_MODE=public_csv
|
|
137
|
+
* - SHEET_CSV_URL_BUSINESS
|
|
138
|
+
* - SHEET_CSV_URL_HOURS (optional)
|
|
139
|
+
* - SHEET_CSV_URL_SERVICES (optional)
|
|
140
|
+
* - SHEET_CSV_URL_GALLERY (optional)
|
|
141
|
+
* - SHEET_CSV_URL_TESTIMONIALS (optional)
|
|
142
|
+
* - SHEET_CSV_URL_FAQ (optional)
|
|
143
|
+
* - SHEET_CSV_URL_TEAM (optional)
|
|
144
|
+
* - SHEET_CSV_URL_MENU (optional)
|
|
145
|
+
* - SHEET_CSV_URL_PRODUCTS (optional)
|
|
146
|
+
* - SHEET_CSV_URL_ANNOUNCEMENTS (optional)
|
|
147
|
+
*
|
|
148
|
+
* Expected environment variables for private API mode:
|
|
149
|
+
* - SHEET_MODE=private_api
|
|
150
|
+
* - SHEET_ID
|
|
151
|
+
* - GOOGLE_SERVICE_ACCOUNT_EMAIL
|
|
152
|
+
* - GOOGLE_PRIVATE_KEY
|
|
153
|
+
*
|
|
154
|
+
* For inline mode, use createInlineConfig() instead.
|
|
155
|
+
*/
|
|
156
|
+
export function createSheetConfigFromEnv(): SheetConfig | undefined {
|
|
157
|
+
const mode = process.env.SHEET_MODE;
|
|
158
|
+
|
|
159
|
+
if (mode === 'public_csv') {
|
|
160
|
+
const businessUrl = process.env.SHEET_CSV_URL_BUSINESS;
|
|
161
|
+
if (!businessUrl) {
|
|
162
|
+
console.warn('SHEET_CSV_URL_BUSINESS not set, using defaults');
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
mode: 'public_csv',
|
|
168
|
+
tabs: {
|
|
169
|
+
business: businessUrl,
|
|
170
|
+
hours: process.env.SHEET_CSV_URL_HOURS,
|
|
171
|
+
services: process.env.SHEET_CSV_URL_SERVICES,
|
|
172
|
+
gallery: process.env.SHEET_CSV_URL_GALLERY,
|
|
173
|
+
testimonials: process.env.SHEET_CSV_URL_TESTIMONIALS,
|
|
174
|
+
faq: process.env.SHEET_CSV_URL_FAQ,
|
|
175
|
+
team: process.env.SHEET_CSV_URL_TEAM,
|
|
176
|
+
menu: process.env.SHEET_CSV_URL_MENU,
|
|
177
|
+
products: process.env.SHEET_CSV_URL_PRODUCTS,
|
|
178
|
+
announcements: process.env.SHEET_CSV_URL_ANNOUNCEMENTS,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (mode === 'private_api') {
|
|
184
|
+
const sheetId = process.env.SHEET_ID;
|
|
185
|
+
const clientEmail = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
|
186
|
+
const privateKey = process.env.GOOGLE_PRIVATE_KEY;
|
|
187
|
+
|
|
188
|
+
if (!sheetId || !clientEmail || !privateKey) {
|
|
189
|
+
console.warn('Missing required environment variables for private API mode');
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
mode: 'private_api',
|
|
195
|
+
sheetId,
|
|
196
|
+
clientEmail,
|
|
197
|
+
privateKey,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// No valid mode configured
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// =============================================================================
|
|
206
|
+
// INLINE DATA UTILITIES
|
|
207
|
+
// =============================================================================
|
|
208
|
+
|
|
209
|
+
import type { InlineDataConfig, BusinessInfo, HoursEntry, DayOfWeek } from './types';
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Create an inline configuration for direct data injection.
|
|
213
|
+
* This bypasses Google Sheets entirely and uses provided data directly.
|
|
214
|
+
*
|
|
215
|
+
* Ideal for:
|
|
216
|
+
* - Automated site generation from CSV/JSON leads
|
|
217
|
+
* - Rapid prototyping and demos
|
|
218
|
+
* - Testing without network dependencies
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```ts
|
|
222
|
+
* const config = createInlineConfig({
|
|
223
|
+
* business: {
|
|
224
|
+
* name: "John's Tailor Shop",
|
|
225
|
+
* tagline: "Expert Alterations",
|
|
226
|
+
* phone: "555-123-4567",
|
|
227
|
+
* city: "San Diego",
|
|
228
|
+
* state: "CA"
|
|
229
|
+
* },
|
|
230
|
+
* hours: [
|
|
231
|
+
* { day: 'monday', open: '9:00 AM', close: '5:00 PM', closed: false }
|
|
232
|
+
* ]
|
|
233
|
+
* });
|
|
234
|
+
*
|
|
235
|
+
* const siteData = await getSiteData(config, industryDefaults.tailor());
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function createInlineConfig(data: PartialSiteData): InlineDataConfig {
|
|
239
|
+
return {
|
|
240
|
+
mode: 'inline',
|
|
241
|
+
data,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Input format for lead data from scraped sources (e.g., Google Maps).
|
|
247
|
+
* Designed to match common CSV export formats.
|
|
248
|
+
*/
|
|
249
|
+
export interface LeadData {
|
|
250
|
+
// Required
|
|
251
|
+
name: string;
|
|
252
|
+
|
|
253
|
+
// Contact (at least phone recommended)
|
|
254
|
+
phone?: string;
|
|
255
|
+
phoneAlt?: string;
|
|
256
|
+
email?: string;
|
|
257
|
+
|
|
258
|
+
// Address - can be single field or split
|
|
259
|
+
address?: string; // Full address string (will attempt to parse)
|
|
260
|
+
addressLine1?: string;
|
|
261
|
+
addressLine2?: string;
|
|
262
|
+
city?: string;
|
|
263
|
+
state?: string;
|
|
264
|
+
zip?: string;
|
|
265
|
+
country?: string;
|
|
266
|
+
|
|
267
|
+
// Business info
|
|
268
|
+
tagline?: string;
|
|
269
|
+
description?: string;
|
|
270
|
+
category?: string; // e.g., "Tailor", "Restaurant" - used for preset selection
|
|
271
|
+
googleMapsUrl?: string;
|
|
272
|
+
|
|
273
|
+
// Hours - can be structured or simple
|
|
274
|
+
hours?: HoursEntry[] | SimpleHours;
|
|
275
|
+
|
|
276
|
+
// Images
|
|
277
|
+
heroImageUrl?: string;
|
|
278
|
+
logoUrl?: string;
|
|
279
|
+
galleryUrls?: string[];
|
|
280
|
+
|
|
281
|
+
// Social proof
|
|
282
|
+
rating?: number; // e.g., 4.5
|
|
283
|
+
reviewCount?: number;
|
|
284
|
+
testimonialQuote?: string;
|
|
285
|
+
testimonialAuthor?: string;
|
|
286
|
+
|
|
287
|
+
// Social links
|
|
288
|
+
yelp?: string;
|
|
289
|
+
instagram?: string;
|
|
290
|
+
facebook?: string;
|
|
291
|
+
twitter?: string;
|
|
292
|
+
website?: string;
|
|
293
|
+
|
|
294
|
+
// Additional
|
|
295
|
+
yearEstablished?: number;
|
|
296
|
+
priceRange?: '$' | '$$' | '$$$' | '$$$$';
|
|
297
|
+
timezone?: string;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Simple hours format for common patterns.
|
|
302
|
+
*/
|
|
303
|
+
export interface SimpleHours {
|
|
304
|
+
weekdays?: { open: string; close: string };
|
|
305
|
+
saturday?: { open: string; close: string } | 'closed';
|
|
306
|
+
sunday?: { open: string; close: string } | 'closed';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Convert simple hours format to HoursEntry array.
|
|
311
|
+
*/
|
|
312
|
+
function parseSimpleHours(hours: SimpleHours): HoursEntry[] {
|
|
313
|
+
const entries: HoursEntry[] = [];
|
|
314
|
+
const weekdays: DayOfWeek[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'];
|
|
315
|
+
|
|
316
|
+
if (hours.weekdays) {
|
|
317
|
+
for (const day of weekdays) {
|
|
318
|
+
entries.push({
|
|
319
|
+
day,
|
|
320
|
+
open: hours.weekdays.open,
|
|
321
|
+
close: hours.weekdays.close,
|
|
322
|
+
closed: false,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (hours.saturday) {
|
|
328
|
+
entries.push(
|
|
329
|
+
hours.saturday === 'closed'
|
|
330
|
+
? { day: 'saturday', closed: true }
|
|
331
|
+
: { day: 'saturday', open: hours.saturday.open, close: hours.saturday.close, closed: false }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (hours.sunday) {
|
|
336
|
+
entries.push(
|
|
337
|
+
hours.sunday === 'closed'
|
|
338
|
+
? { day: 'sunday', closed: true }
|
|
339
|
+
: { day: 'sunday', open: hours.sunday.open, close: hours.sunday.close, closed: false }
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return entries;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Parse a full address string into components.
|
|
348
|
+
* Handles common US address formats.
|
|
349
|
+
*/
|
|
350
|
+
function parseAddressString(
|
|
351
|
+
address: string
|
|
352
|
+
): Pick<BusinessInfo, 'addressLine1' | 'city' | 'state' | 'zip'> {
|
|
353
|
+
// Try to match: "123 Main St, City, ST 12345" or similar
|
|
354
|
+
const parts = address.split(',').map((s) => s.trim());
|
|
355
|
+
|
|
356
|
+
if (parts.length >= 3) {
|
|
357
|
+
// Likely: Street, City, State ZIP
|
|
358
|
+
const lastPart = parts[parts.length - 1];
|
|
359
|
+
const stateZipMatch = lastPart.match(/^([A-Z]{2})\s*(\d{5}(?:-\d{4})?)$/);
|
|
360
|
+
|
|
361
|
+
if (stateZipMatch) {
|
|
362
|
+
return {
|
|
363
|
+
addressLine1: parts.slice(0, -2).join(', '),
|
|
364
|
+
city: parts[parts.length - 2],
|
|
365
|
+
state: stateZipMatch[1],
|
|
366
|
+
zip: stateZipMatch[2],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (parts.length === 2) {
|
|
372
|
+
// Likely: Street, City ST ZIP
|
|
373
|
+
const cityStateZipMatch = parts[1].match(/^(.+?)\s+([A-Z]{2})\s*(\d{5}(?:-\d{4})?)$/);
|
|
374
|
+
if (cityStateZipMatch) {
|
|
375
|
+
return {
|
|
376
|
+
addressLine1: parts[0],
|
|
377
|
+
city: cityStateZipMatch[1],
|
|
378
|
+
state: cityStateZipMatch[2],
|
|
379
|
+
zip: cityStateZipMatch[3],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Fallback: just use as address line 1
|
|
385
|
+
return { addressLine1: address };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create PartialSiteData from lead data (e.g., scraped from Google Maps).
|
|
390
|
+
*
|
|
391
|
+
* This is the primary function for converting CSV/JSON lead data into
|
|
392
|
+
* a format suitable for SheetSite. It handles common data formats and
|
|
393
|
+
* performs sensible normalization.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* ```ts
|
|
397
|
+
* const lead = {
|
|
398
|
+
* name: "John's Tailor Shop",
|
|
399
|
+
* phone: "555-123-4567",
|
|
400
|
+
* address: "123 Main St, San Diego, CA 92101",
|
|
401
|
+
* category: "Tailor",
|
|
402
|
+
* rating: 4.8,
|
|
403
|
+
* reviewCount: 67,
|
|
404
|
+
* hours: {
|
|
405
|
+
* weekdays: { open: '9:00 AM', close: '5:00 PM' },
|
|
406
|
+
* saturday: { open: '10:00 AM', close: '3:00 PM' },
|
|
407
|
+
* sunday: 'closed'
|
|
408
|
+
* }
|
|
409
|
+
* };
|
|
410
|
+
*
|
|
411
|
+
* const siteData = createSiteDataFromLead(lead);
|
|
412
|
+
* const config = createInlineConfig(siteData);
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export function createSiteDataFromLead(lead: LeadData): PartialSiteData {
|
|
416
|
+
// Parse address if provided as single string
|
|
417
|
+
const addressParts =
|
|
418
|
+
lead.address && !lead.addressLine1 ? parseAddressString(lead.address) : {};
|
|
419
|
+
|
|
420
|
+
// Build business info
|
|
421
|
+
const business: Partial<BusinessInfo> = {
|
|
422
|
+
name: lead.name,
|
|
423
|
+
tagline: lead.tagline,
|
|
424
|
+
description: lead.description,
|
|
425
|
+
phone: lead.phone,
|
|
426
|
+
phoneAlt: lead.phoneAlt,
|
|
427
|
+
email: lead.email,
|
|
428
|
+
addressLine1: lead.addressLine1 || addressParts.addressLine1,
|
|
429
|
+
addressLine2: lead.addressLine2,
|
|
430
|
+
city: lead.city || addressParts.city,
|
|
431
|
+
state: lead.state || addressParts.state,
|
|
432
|
+
zip: lead.zip || addressParts.zip,
|
|
433
|
+
country: lead.country || 'USA',
|
|
434
|
+
googleMapsUrl: lead.googleMapsUrl,
|
|
435
|
+
timezone: lead.timezone || 'America/Los_Angeles',
|
|
436
|
+
heroImageUrl: lead.heroImageUrl,
|
|
437
|
+
logoUrl: lead.logoUrl,
|
|
438
|
+
socialYelp: lead.yelp,
|
|
439
|
+
socialInstagram: lead.instagram,
|
|
440
|
+
socialFacebook: lead.facebook,
|
|
441
|
+
socialTwitter: lead.twitter,
|
|
442
|
+
yearEstablished: lead.yearEstablished,
|
|
443
|
+
priceRange: lead.priceRange,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Parse hours
|
|
447
|
+
let hours: HoursEntry[] | undefined;
|
|
448
|
+
if (lead.hours) {
|
|
449
|
+
if (Array.isArray(lead.hours)) {
|
|
450
|
+
hours = lead.hours;
|
|
451
|
+
} else {
|
|
452
|
+
hours = parseSimpleHours(lead.hours);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Build gallery from URLs
|
|
457
|
+
const gallery = lead.galleryUrls?.map((url, i) => ({
|
|
458
|
+
id: `gallery-${i}`,
|
|
459
|
+
imageUrl: url,
|
|
460
|
+
alt: `${lead.name} - Image ${i + 1}`,
|
|
461
|
+
featured: false,
|
|
462
|
+
sortOrder: i,
|
|
463
|
+
}));
|
|
464
|
+
|
|
465
|
+
// Build testimonial from rating/review data
|
|
466
|
+
const testimonials =
|
|
467
|
+
lead.testimonialQuote && lead.testimonialAuthor
|
|
468
|
+
? [
|
|
469
|
+
{
|
|
470
|
+
id: 'testimonial-1',
|
|
471
|
+
quote: lead.testimonialQuote,
|
|
472
|
+
name: lead.testimonialAuthor,
|
|
473
|
+
rating: lead.rating,
|
|
474
|
+
source: 'Google',
|
|
475
|
+
featured: false,
|
|
476
|
+
sortOrder: 0,
|
|
477
|
+
},
|
|
478
|
+
]
|
|
479
|
+
: lead.rating
|
|
480
|
+
? [
|
|
481
|
+
{
|
|
482
|
+
id: 'testimonial-1',
|
|
483
|
+
quote: `Rated ${lead.rating}/5 on Google${lead.reviewCount ? ` based on ${lead.reviewCount} reviews` : ''}`,
|
|
484
|
+
name: 'Google Reviews',
|
|
485
|
+
rating: Math.round(lead.rating),
|
|
486
|
+
source: 'Google',
|
|
487
|
+
featured: false,
|
|
488
|
+
sortOrder: 0,
|
|
489
|
+
},
|
|
490
|
+
]
|
|
491
|
+
: undefined;
|
|
492
|
+
|
|
493
|
+
return {
|
|
494
|
+
business,
|
|
495
|
+
hours,
|
|
496
|
+
gallery,
|
|
497
|
+
testimonials,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Create a complete site configuration from lead data in one step.
|
|
503
|
+
*
|
|
504
|
+
* Combines createSiteDataFromLead with defaults for the specified business type.
|
|
505
|
+
*
|
|
506
|
+
* @param lead - Lead data from scraping
|
|
507
|
+
* @param businessType - Business type for defaults (e.g., 'tailor', 'restaurant')
|
|
508
|
+
* @returns Complete site data ready for rendering
|
|
509
|
+
*
|
|
510
|
+
* @example
|
|
511
|
+
* ```ts
|
|
512
|
+
* const lead = { name: "Maria's Alterations", phone: "555-1234", category: "tailor" };
|
|
513
|
+
* const siteData = await createSiteFromLead(lead, 'tailor');
|
|
514
|
+
* ```
|
|
515
|
+
*/
|
|
516
|
+
export async function createSiteFromLead(
|
|
517
|
+
lead: LeadData,
|
|
518
|
+
businessType?: string
|
|
519
|
+
): Promise<SiteData> {
|
|
520
|
+
const partialData = createSiteDataFromLead(lead);
|
|
521
|
+
const config = createInlineConfig(partialData);
|
|
522
|
+
|
|
523
|
+
// Get industry-specific defaults if available
|
|
524
|
+
const type = businessType || lead.category?.toLowerCase().replace(/\s+/g, '-');
|
|
525
|
+
let defaults = DEFAULT_SITE_DATA;
|
|
526
|
+
|
|
527
|
+
if (type) {
|
|
528
|
+
const typeKey = type as keyof typeof industryDefaults;
|
|
529
|
+
if (typeKey in industryDefaults) {
|
|
530
|
+
defaults = industryDefaults[typeKey]();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return getSiteData(config, defaults);
|
|
535
|
+
}
|