@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.
Files changed (57) hide show
  1. package/README.md +105 -0
  2. package/dist/components/index.js +1696 -0
  3. package/dist/components/index.js.map +1 -0
  4. package/dist/components/index.mjs +1630 -0
  5. package/dist/components/index.mjs.map +1 -0
  6. package/dist/config/index.js +1840 -0
  7. package/dist/config/index.js.map +1 -0
  8. package/dist/config/index.mjs +1793 -0
  9. package/dist/config/index.mjs.map +1 -0
  10. package/dist/data/index.js +1296 -0
  11. package/dist/data/index.js.map +1 -0
  12. package/dist/data/index.mjs +1220 -0
  13. package/dist/data/index.mjs.map +1 -0
  14. package/dist/index.js +5433 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/index.mjs +5285 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/seo/index.js +187 -0
  19. package/dist/seo/index.js.map +1 -0
  20. package/dist/seo/index.mjs +155 -0
  21. package/dist/seo/index.mjs.map +1 -0
  22. package/dist/theme/index.js +552 -0
  23. package/dist/theme/index.js.map +1 -0
  24. package/dist/theme/index.mjs +526 -0
  25. package/dist/theme/index.mjs.map +1 -0
  26. package/package.json +96 -0
  27. package/src/components/index.ts +41 -0
  28. package/src/components/layout/Footer.tsx +234 -0
  29. package/src/components/layout/Header.tsx +134 -0
  30. package/src/components/sections/FAQ.tsx +178 -0
  31. package/src/components/sections/Gallery.tsx +107 -0
  32. package/src/components/sections/Hero.tsx +202 -0
  33. package/src/components/sections/Hours.tsx +225 -0
  34. package/src/components/sections/Services.tsx +216 -0
  35. package/src/components/sections/Testimonials.tsx +184 -0
  36. package/src/components/ui/Button.tsx +158 -0
  37. package/src/components/ui/Card.tsx +162 -0
  38. package/src/components/ui/Icons.tsx +508 -0
  39. package/src/config/index.ts +207 -0
  40. package/src/config/presets/generic.ts +153 -0
  41. package/src/config/presets/home-kitchen.ts +154 -0
  42. package/src/config/presets/index.ts +708 -0
  43. package/src/config/presets/professional.ts +165 -0
  44. package/src/config/presets/repair.ts +160 -0
  45. package/src/config/presets/restaurant.ts +162 -0
  46. package/src/config/presets/salon.ts +178 -0
  47. package/src/config/presets/tailor.ts +159 -0
  48. package/src/config/types.ts +314 -0
  49. package/src/data/csv-parser.ts +154 -0
  50. package/src/data/defaults.ts +202 -0
  51. package/src/data/google-drive.ts +148 -0
  52. package/src/data/index.ts +535 -0
  53. package/src/data/sheets.ts +709 -0
  54. package/src/data/types.ts +379 -0
  55. package/src/seo/index.ts +272 -0
  56. package/src/theme/colors.ts +351 -0
  57. 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
+ }