@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,1296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/data/index.ts
|
|
21
|
+
var data_exports = {};
|
|
22
|
+
__export(data_exports, {
|
|
23
|
+
AnnouncementSchema: () => AnnouncementSchema,
|
|
24
|
+
AnnouncementsSchema: () => AnnouncementsSchema,
|
|
25
|
+
BusinessInfoSchema: () => BusinessInfoSchema,
|
|
26
|
+
DEFAULT_SITE_DATA: () => DEFAULT_SITE_DATA,
|
|
27
|
+
DayOfWeek: () => DayOfWeek,
|
|
28
|
+
FAQItemSchema: () => FAQItemSchema,
|
|
29
|
+
FAQSchema: () => FAQSchema,
|
|
30
|
+
GalleryItemSchema: () => GalleryItemSchema,
|
|
31
|
+
GallerySchema: () => GallerySchema,
|
|
32
|
+
HoursEntrySchema: () => HoursEntrySchema,
|
|
33
|
+
HoursSchema: () => HoursSchema,
|
|
34
|
+
MenuItemSchema: () => MenuItemSchema,
|
|
35
|
+
MenuSchema: () => MenuSchema,
|
|
36
|
+
PartialSiteDataSchema: () => PartialSiteDataSchema,
|
|
37
|
+
ProductSchema: () => ProductSchema,
|
|
38
|
+
ProductsSchema: () => ProductsSchema,
|
|
39
|
+
ServiceSchema: () => ServiceSchema,
|
|
40
|
+
ServicesSchema: () => ServicesSchema,
|
|
41
|
+
SiteDataSchema: () => SiteDataSchema,
|
|
42
|
+
TeamMemberSchema: () => TeamMemberSchema,
|
|
43
|
+
TeamSchema: () => TeamSchema,
|
|
44
|
+
TestimonialSchema: () => TestimonialSchema,
|
|
45
|
+
TestimonialsSchema: () => TestimonialsSchema,
|
|
46
|
+
clearSiteDataCache: () => clearSiteDataCache,
|
|
47
|
+
createDefaultSiteData: () => createDefaultSiteData,
|
|
48
|
+
createInlineConfig: () => createInlineConfig,
|
|
49
|
+
createSheetConfigFromEnv: () => createSheetConfigFromEnv,
|
|
50
|
+
createSiteDataFromLead: () => createSiteDataFromLead,
|
|
51
|
+
createSiteFromLead: () => createSiteFromLead,
|
|
52
|
+
extractGoogleDriveFileId: () => extractGoogleDriveFileId,
|
|
53
|
+
fetchInlineData: () => fetchInlineData,
|
|
54
|
+
fetchPrivateSheetData: () => fetchPrivateSheetData,
|
|
55
|
+
fetchPublicSheetData: () => fetchPublicSheetData,
|
|
56
|
+
fetchSheetData: () => fetchSheetData,
|
|
57
|
+
getGoogleDriveThumbnail: () => getGoogleDriveThumbnail,
|
|
58
|
+
getSiteData: () => getSiteData,
|
|
59
|
+
getSiteDataWithName: () => getSiteDataWithName,
|
|
60
|
+
industryDefaults: () => industryDefaults,
|
|
61
|
+
isGoogleDriveUrl: () => isGoogleDriveUrl,
|
|
62
|
+
mapRows: () => mapRows,
|
|
63
|
+
mergeSiteData: () => mergeSiteData,
|
|
64
|
+
normalizeGoogleDriveUrl: () => normalizeGoogleDriveUrl,
|
|
65
|
+
normalizeHeader: () => normalizeHeader,
|
|
66
|
+
parseBoolean: () => parseBoolean,
|
|
67
|
+
parseCSV: () => parseCSV,
|
|
68
|
+
parseInteger: () => parseInteger,
|
|
69
|
+
parseNumber: () => parseNumber,
|
|
70
|
+
parseSortOrder: () => parseSortOrder,
|
|
71
|
+
processImageUrls: () => processImageUrls,
|
|
72
|
+
processImageUrlsInArray: () => processImageUrlsInArray
|
|
73
|
+
});
|
|
74
|
+
module.exports = __toCommonJS(data_exports);
|
|
75
|
+
|
|
76
|
+
// src/data/types.ts
|
|
77
|
+
var import_zod = require("zod");
|
|
78
|
+
var BusinessInfoSchema = import_zod.z.object({
|
|
79
|
+
name: import_zod.z.string().min(1),
|
|
80
|
+
tagline: import_zod.z.string().optional(),
|
|
81
|
+
description: import_zod.z.string().optional(),
|
|
82
|
+
aboutShort: import_zod.z.string().optional(),
|
|
83
|
+
aboutLong: import_zod.z.string().optional(),
|
|
84
|
+
// Contact
|
|
85
|
+
phone: import_zod.z.string().optional(),
|
|
86
|
+
phoneAlt: import_zod.z.string().optional(),
|
|
87
|
+
email: import_zod.z.string().email().optional().or(import_zod.z.literal("")),
|
|
88
|
+
// Address
|
|
89
|
+
addressLine1: import_zod.z.string().optional(),
|
|
90
|
+
addressLine2: import_zod.z.string().optional(),
|
|
91
|
+
city: import_zod.z.string().optional(),
|
|
92
|
+
state: import_zod.z.string().optional(),
|
|
93
|
+
zip: import_zod.z.string().optional(),
|
|
94
|
+
country: import_zod.z.string().default("USA"),
|
|
95
|
+
// Location
|
|
96
|
+
googleMapsUrl: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
97
|
+
mapsEmbedUrl: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
98
|
+
timezone: import_zod.z.string().default("America/Los_Angeles"),
|
|
99
|
+
// Images
|
|
100
|
+
logoUrl: import_zod.z.string().optional(),
|
|
101
|
+
heroImageUrl: import_zod.z.string().optional(),
|
|
102
|
+
ogImageUrl: import_zod.z.string().optional(),
|
|
103
|
+
// Social
|
|
104
|
+
socialYelp: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
105
|
+
socialInstagram: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
106
|
+
socialFacebook: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
107
|
+
socialTwitter: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
108
|
+
socialLinkedIn: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
109
|
+
socialYoutube: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
110
|
+
socialTiktok: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
111
|
+
// CTAs
|
|
112
|
+
primaryCtaText: import_zod.z.string().optional(),
|
|
113
|
+
primaryCtaUrl: import_zod.z.string().optional(),
|
|
114
|
+
secondaryCtaText: import_zod.z.string().optional(),
|
|
115
|
+
secondaryCtaUrl: import_zod.z.string().optional(),
|
|
116
|
+
bookingUrl: import_zod.z.string().url().optional().or(import_zod.z.literal("")),
|
|
117
|
+
// Banner
|
|
118
|
+
bannerEnabled: import_zod.z.boolean().default(false),
|
|
119
|
+
bannerText: import_zod.z.string().optional(),
|
|
120
|
+
bannerUrl: import_zod.z.string().optional(),
|
|
121
|
+
// Business metadata
|
|
122
|
+
priceRange: import_zod.z.enum(["$", "$$", "$$$", "$$$$"]).optional(),
|
|
123
|
+
yearEstablished: import_zod.z.number().optional(),
|
|
124
|
+
licenseNumber: import_zod.z.string().optional()
|
|
125
|
+
});
|
|
126
|
+
var DayOfWeek = import_zod.z.enum(["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]);
|
|
127
|
+
var HoursEntrySchema = import_zod.z.object({
|
|
128
|
+
day: DayOfWeek,
|
|
129
|
+
open: import_zod.z.string().optional(),
|
|
130
|
+
// e.g., "9:00 AM"
|
|
131
|
+
close: import_zod.z.string().optional(),
|
|
132
|
+
// e.g., "5:00 PM"
|
|
133
|
+
closed: import_zod.z.boolean().default(false)
|
|
134
|
+
});
|
|
135
|
+
var HoursSchema = import_zod.z.array(HoursEntrySchema);
|
|
136
|
+
var ServiceSchema = import_zod.z.object({
|
|
137
|
+
id: import_zod.z.string().optional(),
|
|
138
|
+
title: import_zod.z.string().min(1),
|
|
139
|
+
description: import_zod.z.string().optional(),
|
|
140
|
+
priceNote: import_zod.z.string().optional(),
|
|
141
|
+
// e.g., "Starting at $15" or "Call for quote"
|
|
142
|
+
price: import_zod.z.number().optional(),
|
|
143
|
+
// Exact price if applicable
|
|
144
|
+
duration: import_zod.z.string().optional(),
|
|
145
|
+
// e.g., "30 min"
|
|
146
|
+
icon: import_zod.z.string().optional(),
|
|
147
|
+
// Icon identifier
|
|
148
|
+
imageUrl: import_zod.z.string().optional(),
|
|
149
|
+
featured: import_zod.z.boolean().default(false),
|
|
150
|
+
sortOrder: import_zod.z.number().default(0),
|
|
151
|
+
category: import_zod.z.string().optional()
|
|
152
|
+
});
|
|
153
|
+
var ServicesSchema = import_zod.z.array(ServiceSchema);
|
|
154
|
+
var GalleryItemSchema = import_zod.z.object({
|
|
155
|
+
id: import_zod.z.string().optional(),
|
|
156
|
+
imageUrl: import_zod.z.string().min(1),
|
|
157
|
+
alt: import_zod.z.string().optional(),
|
|
158
|
+
caption: import_zod.z.string().optional(),
|
|
159
|
+
featured: import_zod.z.boolean().default(false),
|
|
160
|
+
sortOrder: import_zod.z.number().default(0),
|
|
161
|
+
category: import_zod.z.string().optional()
|
|
162
|
+
});
|
|
163
|
+
var GallerySchema = import_zod.z.array(GalleryItemSchema);
|
|
164
|
+
var TestimonialSchema = import_zod.z.object({
|
|
165
|
+
id: import_zod.z.string().optional(),
|
|
166
|
+
quote: import_zod.z.string().min(1),
|
|
167
|
+
name: import_zod.z.string().min(1),
|
|
168
|
+
context: import_zod.z.string().optional(),
|
|
169
|
+
// e.g., "Wedding alterations"
|
|
170
|
+
rating: import_zod.z.number().min(1).max(5).optional(),
|
|
171
|
+
source: import_zod.z.string().optional(),
|
|
172
|
+
// e.g., "Google", "Yelp"
|
|
173
|
+
date: import_zod.z.string().optional(),
|
|
174
|
+
imageUrl: import_zod.z.string().optional(),
|
|
175
|
+
// Reviewer photo
|
|
176
|
+
featured: import_zod.z.boolean().default(false),
|
|
177
|
+
sortOrder: import_zod.z.number().default(0)
|
|
178
|
+
});
|
|
179
|
+
var TestimonialsSchema = import_zod.z.array(TestimonialSchema);
|
|
180
|
+
var FAQItemSchema = import_zod.z.object({
|
|
181
|
+
id: import_zod.z.string().optional(),
|
|
182
|
+
question: import_zod.z.string().min(1),
|
|
183
|
+
answer: import_zod.z.string().min(1),
|
|
184
|
+
category: import_zod.z.string().optional(),
|
|
185
|
+
sortOrder: import_zod.z.number().default(0)
|
|
186
|
+
});
|
|
187
|
+
var FAQSchema = import_zod.z.array(FAQItemSchema);
|
|
188
|
+
var TeamMemberSchema = import_zod.z.object({
|
|
189
|
+
id: import_zod.z.string().optional(),
|
|
190
|
+
name: import_zod.z.string().min(1),
|
|
191
|
+
role: import_zod.z.string().optional(),
|
|
192
|
+
bio: import_zod.z.string().optional(),
|
|
193
|
+
imageUrl: import_zod.z.string().optional(),
|
|
194
|
+
email: import_zod.z.string().email().optional().or(import_zod.z.literal("")),
|
|
195
|
+
phone: import_zod.z.string().optional(),
|
|
196
|
+
sortOrder: import_zod.z.number().default(0)
|
|
197
|
+
});
|
|
198
|
+
var TeamSchema = import_zod.z.array(TeamMemberSchema);
|
|
199
|
+
var MenuItemSchema = import_zod.z.object({
|
|
200
|
+
id: import_zod.z.string().optional(),
|
|
201
|
+
name: import_zod.z.string().min(1),
|
|
202
|
+
description: import_zod.z.string().optional(),
|
|
203
|
+
price: import_zod.z.number().optional(),
|
|
204
|
+
priceNote: import_zod.z.string().optional(),
|
|
205
|
+
category: import_zod.z.string().optional(),
|
|
206
|
+
imageUrl: import_zod.z.string().optional(),
|
|
207
|
+
dietary: import_zod.z.array(import_zod.z.string()).optional(),
|
|
208
|
+
// e.g., ["vegetarian", "gluten-free"]
|
|
209
|
+
featured: import_zod.z.boolean().default(false),
|
|
210
|
+
available: import_zod.z.boolean().default(true),
|
|
211
|
+
sortOrder: import_zod.z.number().default(0)
|
|
212
|
+
});
|
|
213
|
+
var MenuSchema = import_zod.z.array(MenuItemSchema);
|
|
214
|
+
var ProductSchema = import_zod.z.object({
|
|
215
|
+
id: import_zod.z.string().optional(),
|
|
216
|
+
name: import_zod.z.string().min(1),
|
|
217
|
+
description: import_zod.z.string().optional(),
|
|
218
|
+
price: import_zod.z.number().optional(),
|
|
219
|
+
priceNote: import_zod.z.string().optional(),
|
|
220
|
+
category: import_zod.z.string().optional(),
|
|
221
|
+
imageUrl: import_zod.z.string().optional(),
|
|
222
|
+
images: import_zod.z.array(import_zod.z.string()).optional(),
|
|
223
|
+
inStock: import_zod.z.boolean().default(true),
|
|
224
|
+
featured: import_zod.z.boolean().default(false),
|
|
225
|
+
sortOrder: import_zod.z.number().default(0),
|
|
226
|
+
sku: import_zod.z.string().optional(),
|
|
227
|
+
purchaseUrl: import_zod.z.string().url().optional().or(import_zod.z.literal(""))
|
|
228
|
+
});
|
|
229
|
+
var ProductsSchema = import_zod.z.array(ProductSchema);
|
|
230
|
+
var AnnouncementSchema = import_zod.z.object({
|
|
231
|
+
id: import_zod.z.string().optional(),
|
|
232
|
+
title: import_zod.z.string().min(1),
|
|
233
|
+
content: import_zod.z.string().optional(),
|
|
234
|
+
imageUrl: import_zod.z.string().optional(),
|
|
235
|
+
linkUrl: import_zod.z.string().optional(),
|
|
236
|
+
linkText: import_zod.z.string().optional(),
|
|
237
|
+
startDate: import_zod.z.string().optional(),
|
|
238
|
+
endDate: import_zod.z.string().optional(),
|
|
239
|
+
active: import_zod.z.boolean().default(true),
|
|
240
|
+
sortOrder: import_zod.z.number().default(0)
|
|
241
|
+
});
|
|
242
|
+
var AnnouncementsSchema = import_zod.z.array(AnnouncementSchema);
|
|
243
|
+
var SiteDataSchema = import_zod.z.object({
|
|
244
|
+
business: BusinessInfoSchema,
|
|
245
|
+
hours: HoursSchema,
|
|
246
|
+
services: ServicesSchema,
|
|
247
|
+
gallery: GallerySchema,
|
|
248
|
+
testimonials: TestimonialsSchema,
|
|
249
|
+
faq: FAQSchema,
|
|
250
|
+
team: TeamSchema.optional(),
|
|
251
|
+
menu: MenuSchema.optional(),
|
|
252
|
+
products: ProductsSchema.optional(),
|
|
253
|
+
announcements: AnnouncementsSchema.optional()
|
|
254
|
+
});
|
|
255
|
+
var PartialSiteDataSchema = import_zod.z.object({
|
|
256
|
+
business: BusinessInfoSchema.partial().optional(),
|
|
257
|
+
hours: HoursSchema.optional(),
|
|
258
|
+
services: ServicesSchema.optional(),
|
|
259
|
+
gallery: GallerySchema.optional(),
|
|
260
|
+
testimonials: TestimonialsSchema.optional(),
|
|
261
|
+
faq: FAQSchema.optional(),
|
|
262
|
+
team: TeamSchema.optional(),
|
|
263
|
+
menu: MenuSchema.optional(),
|
|
264
|
+
products: ProductsSchema.optional(),
|
|
265
|
+
announcements: AnnouncementsSchema.optional()
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// src/data/csv-parser.ts
|
|
269
|
+
function parseCSV(csvText) {
|
|
270
|
+
const lines = csvText.split(/\r?\n/);
|
|
271
|
+
if (lines.length < 2) return [];
|
|
272
|
+
const headers = parseCSVLine(lines[0]).map(normalizeHeader);
|
|
273
|
+
if (headers.length === 0) return [];
|
|
274
|
+
const rows = [];
|
|
275
|
+
for (let i = 1; i < lines.length; i++) {
|
|
276
|
+
const line = lines[i].trim();
|
|
277
|
+
if (!line) continue;
|
|
278
|
+
const values = parseCSVLine(line);
|
|
279
|
+
const row = {};
|
|
280
|
+
for (let j = 0; j < headers.length; j++) {
|
|
281
|
+
row[headers[j]] = values[j]?.trim() ?? "";
|
|
282
|
+
}
|
|
283
|
+
rows.push(row);
|
|
284
|
+
}
|
|
285
|
+
return rows;
|
|
286
|
+
}
|
|
287
|
+
function parseCSVLine(line) {
|
|
288
|
+
const values = [];
|
|
289
|
+
let current = "";
|
|
290
|
+
let inQuotes = false;
|
|
291
|
+
let i = 0;
|
|
292
|
+
while (i < line.length) {
|
|
293
|
+
const char = line[i];
|
|
294
|
+
const nextChar = line[i + 1];
|
|
295
|
+
if (inQuotes) {
|
|
296
|
+
if (char === '"') {
|
|
297
|
+
if (nextChar === '"') {
|
|
298
|
+
current += '"';
|
|
299
|
+
i += 2;
|
|
300
|
+
} else {
|
|
301
|
+
inQuotes = false;
|
|
302
|
+
i++;
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
current += char;
|
|
306
|
+
i++;
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
if (char === '"') {
|
|
310
|
+
inQuotes = true;
|
|
311
|
+
i++;
|
|
312
|
+
} else if (char === ",") {
|
|
313
|
+
values.push(current);
|
|
314
|
+
current = "";
|
|
315
|
+
i++;
|
|
316
|
+
} else {
|
|
317
|
+
current += char;
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
values.push(current);
|
|
323
|
+
return values;
|
|
324
|
+
}
|
|
325
|
+
function normalizeHeader(header) {
|
|
326
|
+
return header.toLowerCase().trim().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "");
|
|
327
|
+
}
|
|
328
|
+
function mapRows(rows, mapper) {
|
|
329
|
+
return rows.map((row, index) => mapper(row, index)).filter((item) => item !== null);
|
|
330
|
+
}
|
|
331
|
+
function parseBoolean(value) {
|
|
332
|
+
if (!value) return false;
|
|
333
|
+
const lower = value.toLowerCase().trim();
|
|
334
|
+
return lower === "true" || lower === "yes" || lower === "1";
|
|
335
|
+
}
|
|
336
|
+
function parseNumber(value) {
|
|
337
|
+
if (!value) return void 0;
|
|
338
|
+
const num = parseFloat(value);
|
|
339
|
+
return isNaN(num) ? void 0 : num;
|
|
340
|
+
}
|
|
341
|
+
function parseInteger(value) {
|
|
342
|
+
if (!value) return void 0;
|
|
343
|
+
const num = parseInt(value, 10);
|
|
344
|
+
return isNaN(num) ? void 0 : num;
|
|
345
|
+
}
|
|
346
|
+
function parseSortOrder(value) {
|
|
347
|
+
return parseInteger(value) ?? 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/data/google-drive.ts
|
|
351
|
+
function normalizeGoogleDriveUrl(url) {
|
|
352
|
+
if (!url) return void 0;
|
|
353
|
+
if (!url.includes("drive.google.com")) {
|
|
354
|
+
return url;
|
|
355
|
+
}
|
|
356
|
+
let fileId = null;
|
|
357
|
+
const filePattern = /\/file\/d\/([a-zA-Z0-9_-]+)/;
|
|
358
|
+
const fileMatch = url.match(filePattern);
|
|
359
|
+
if (fileMatch) {
|
|
360
|
+
fileId = fileMatch[1];
|
|
361
|
+
}
|
|
362
|
+
if (!fileId) {
|
|
363
|
+
const idPattern = /[?&]id=([a-zA-Z0-9_-]+)/;
|
|
364
|
+
const idMatch = url.match(idPattern);
|
|
365
|
+
if (idMatch) {
|
|
366
|
+
fileId = idMatch[1];
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!fileId) {
|
|
370
|
+
const openPattern = /\/open\?id=([a-zA-Z0-9_-]+)/;
|
|
371
|
+
const openMatch = url.match(openPattern);
|
|
372
|
+
if (openMatch) {
|
|
373
|
+
fileId = openMatch[1];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!fileId) {
|
|
377
|
+
return url;
|
|
378
|
+
}
|
|
379
|
+
return `https://drive.google.com/uc?export=view&id=${fileId}`;
|
|
380
|
+
}
|
|
381
|
+
function getGoogleDriveThumbnail(url, size = 400) {
|
|
382
|
+
if (!url) return void 0;
|
|
383
|
+
if (!url.includes("drive.google.com")) {
|
|
384
|
+
return url;
|
|
385
|
+
}
|
|
386
|
+
const normalizedUrl = normalizeGoogleDriveUrl(url);
|
|
387
|
+
if (!normalizedUrl) return void 0;
|
|
388
|
+
const idMatch = normalizedUrl.match(/id=([a-zA-Z0-9_-]+)/);
|
|
389
|
+
if (!idMatch) return void 0;
|
|
390
|
+
return `https://drive.google.com/thumbnail?id=${idMatch[1]}&sz=w${size}`;
|
|
391
|
+
}
|
|
392
|
+
function isGoogleDriveUrl(url) {
|
|
393
|
+
if (!url) return false;
|
|
394
|
+
return url.includes("drive.google.com");
|
|
395
|
+
}
|
|
396
|
+
function extractGoogleDriveFileId(url) {
|
|
397
|
+
if (!url) return null;
|
|
398
|
+
const filePattern = /\/file\/d\/([a-zA-Z0-9_-]+)/;
|
|
399
|
+
const fileMatch = url.match(filePattern);
|
|
400
|
+
if (fileMatch) return fileMatch[1];
|
|
401
|
+
const idPattern = /[?&]id=([a-zA-Z0-9_-]+)/;
|
|
402
|
+
const idMatch = url.match(idPattern);
|
|
403
|
+
if (idMatch) return idMatch[1];
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
function processImageUrls(obj, imageFields = ["imageUrl", "logoUrl", "heroImageUrl", "ogImageUrl"]) {
|
|
407
|
+
const result = { ...obj };
|
|
408
|
+
for (const field of imageFields) {
|
|
409
|
+
if (field in result && typeof result[field] === "string") {
|
|
410
|
+
result[field] = normalizeGoogleDriveUrl(result[field]);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
function processImageUrlsInArray(arr, imageFields = ["imageUrl", "image_url", "url"]) {
|
|
416
|
+
return arr.map((item) => processImageUrls(item, imageFields));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/data/sheets.ts
|
|
420
|
+
var DEFAULT_REVALIDATE = 300;
|
|
421
|
+
async function fetchCSV(url, options = {}) {
|
|
422
|
+
const response = await fetch(url, {
|
|
423
|
+
next: { revalidate: options.revalidate ?? DEFAULT_REVALIDATE },
|
|
424
|
+
cache: options.cache
|
|
425
|
+
});
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
throw new Error(`Failed to fetch CSV: ${response.status} ${response.statusText}`);
|
|
428
|
+
}
|
|
429
|
+
return response.text();
|
|
430
|
+
}
|
|
431
|
+
async function fetchPublicTab(url, parser, options = {}) {
|
|
432
|
+
if (!url) return void 0;
|
|
433
|
+
try {
|
|
434
|
+
const csvText = await fetchCSV(url, options);
|
|
435
|
+
const rows = parseCSV(csvText);
|
|
436
|
+
return parser(rows);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error(`Error fetching public tab from ${url}:`, error);
|
|
439
|
+
return void 0;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
async function fetchPublicSheetData(config, options = {}) {
|
|
443
|
+
const [business, hours, services, gallery, testimonials, faq, team, menu, products, announcements] = await Promise.all([
|
|
444
|
+
fetchPublicTab(config.tabs.business, parseBusinessRows, options),
|
|
445
|
+
fetchPublicTab(config.tabs.hours, parseHoursRows, options),
|
|
446
|
+
fetchPublicTab(config.tabs.services, parseServicesRows, options),
|
|
447
|
+
fetchPublicTab(config.tabs.gallery, parseGalleryRows, options),
|
|
448
|
+
fetchPublicTab(config.tabs.testimonials, parseTestimonialsRows, options),
|
|
449
|
+
fetchPublicTab(config.tabs.faq, parseFAQRows, options),
|
|
450
|
+
fetchPublicTab(config.tabs.team, parseTeamRows, options),
|
|
451
|
+
fetchPublicTab(config.tabs.menu, parseMenuRows, options),
|
|
452
|
+
fetchPublicTab(config.tabs.products, parseProductsRows, options),
|
|
453
|
+
fetchPublicTab(config.tabs.announcements, parseAnnouncementsRows, options)
|
|
454
|
+
]);
|
|
455
|
+
return {
|
|
456
|
+
business,
|
|
457
|
+
hours,
|
|
458
|
+
services,
|
|
459
|
+
gallery,
|
|
460
|
+
testimonials,
|
|
461
|
+
faq,
|
|
462
|
+
team,
|
|
463
|
+
menu,
|
|
464
|
+
products,
|
|
465
|
+
announcements
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async function createJWT(clientEmail, privateKey) {
|
|
469
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
470
|
+
const header = {
|
|
471
|
+
alg: "RS256",
|
|
472
|
+
typ: "JWT"
|
|
473
|
+
};
|
|
474
|
+
const payload = {
|
|
475
|
+
iss: clientEmail,
|
|
476
|
+
scope: "https://www.googleapis.com/auth/spreadsheets.readonly",
|
|
477
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
478
|
+
iat: now,
|
|
479
|
+
exp: now + 3600
|
|
480
|
+
// 1 hour
|
|
481
|
+
};
|
|
482
|
+
const encoder = new TextEncoder();
|
|
483
|
+
const headerB64 = btoa(JSON.stringify(header)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
484
|
+
const payloadB64 = btoa(JSON.stringify(payload)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
485
|
+
const signInput = `${headerB64}.${payloadB64}`;
|
|
486
|
+
const pemContents = privateKey.replace(/-----BEGIN PRIVATE KEY-----/g, "").replace(/-----END PRIVATE KEY-----/g, "").replace(/\\n/g, "").replace(/\n/g, "").replace(/\s/g, "");
|
|
487
|
+
const binaryKey = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
488
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
489
|
+
"pkcs8",
|
|
490
|
+
binaryKey,
|
|
491
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
492
|
+
false,
|
|
493
|
+
["sign"]
|
|
494
|
+
);
|
|
495
|
+
const signature = await crypto.subtle.sign(
|
|
496
|
+
"RSASSA-PKCS1-v1_5",
|
|
497
|
+
cryptoKey,
|
|
498
|
+
encoder.encode(signInput)
|
|
499
|
+
);
|
|
500
|
+
const signatureB64 = btoa(String.fromCharCode(...new Uint8Array(signature))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
501
|
+
return `${signInput}.${signatureB64}`;
|
|
502
|
+
}
|
|
503
|
+
async function getAccessToken(clientEmail, privateKey) {
|
|
504
|
+
const jwt = await createJWT(clientEmail, privateKey);
|
|
505
|
+
const response = await fetch("https://oauth2.googleapis.com/token", {
|
|
506
|
+
method: "POST",
|
|
507
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
508
|
+
body: new URLSearchParams({
|
|
509
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
510
|
+
assertion: jwt
|
|
511
|
+
})
|
|
512
|
+
});
|
|
513
|
+
if (!response.ok) {
|
|
514
|
+
throw new Error(`Failed to get access token: ${response.status}`);
|
|
515
|
+
}
|
|
516
|
+
const data = await response.json();
|
|
517
|
+
return data.access_token;
|
|
518
|
+
}
|
|
519
|
+
async function fetchPrivateTab(sheetId, tabName, accessToken, parser) {
|
|
520
|
+
try {
|
|
521
|
+
const url = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${encodeURIComponent(tabName)}`;
|
|
522
|
+
const response = await fetch(url, {
|
|
523
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
524
|
+
});
|
|
525
|
+
if (!response.ok) {
|
|
526
|
+
if (response.status === 404) {
|
|
527
|
+
return void 0;
|
|
528
|
+
}
|
|
529
|
+
throw new Error(`Failed to fetch tab ${tabName}: ${response.status}`);
|
|
530
|
+
}
|
|
531
|
+
const data = await response.json();
|
|
532
|
+
const values = data.values || [];
|
|
533
|
+
if (values.length < 2) return void 0;
|
|
534
|
+
const headers = values[0].map(
|
|
535
|
+
(h) => h.toLowerCase().trim().replace(/\s+/g, "_").replace(/[^a-z0-9_]/g, "")
|
|
536
|
+
);
|
|
537
|
+
const rows = [];
|
|
538
|
+
for (let i = 1; i < values.length; i++) {
|
|
539
|
+
const row = {};
|
|
540
|
+
for (let j = 0; j < headers.length; j++) {
|
|
541
|
+
row[headers[j]] = values[i][j]?.trim() ?? "";
|
|
542
|
+
}
|
|
543
|
+
rows.push(row);
|
|
544
|
+
}
|
|
545
|
+
return parser(rows);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.error(`Error fetching private tab ${tabName}:`, error);
|
|
548
|
+
return void 0;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function fetchPrivateSheetData(config, _options = {}) {
|
|
552
|
+
const accessToken = await getAccessToken(config.clientEmail, config.privateKey);
|
|
553
|
+
const tabs = config.tabs || {};
|
|
554
|
+
const sheetId = config.sheetId;
|
|
555
|
+
const [business, hours, services, gallery, testimonials, faq, team, menu, products, announcements] = await Promise.all([
|
|
556
|
+
fetchPrivateTab(sheetId, tabs.business || "business", accessToken, parseBusinessRows),
|
|
557
|
+
fetchPrivateTab(sheetId, tabs.hours || "hours", accessToken, parseHoursRows),
|
|
558
|
+
fetchPrivateTab(sheetId, tabs.services || "services", accessToken, parseServicesRows),
|
|
559
|
+
fetchPrivateTab(sheetId, tabs.gallery || "gallery", accessToken, parseGalleryRows),
|
|
560
|
+
fetchPrivateTab(sheetId, tabs.testimonials || "testimonials", accessToken, parseTestimonialsRows),
|
|
561
|
+
fetchPrivateTab(sheetId, tabs.faq || "faq", accessToken, parseFAQRows),
|
|
562
|
+
fetchPrivateTab(sheetId, tabs.team || "team", accessToken, parseTeamRows),
|
|
563
|
+
fetchPrivateTab(sheetId, tabs.menu || "menu", accessToken, parseMenuRows),
|
|
564
|
+
fetchPrivateTab(sheetId, tabs.products || "products", accessToken, parseProductsRows),
|
|
565
|
+
fetchPrivateTab(sheetId, tabs.announcements || "announcements", accessToken, parseAnnouncementsRows)
|
|
566
|
+
]);
|
|
567
|
+
return {
|
|
568
|
+
business,
|
|
569
|
+
hours,
|
|
570
|
+
services,
|
|
571
|
+
gallery,
|
|
572
|
+
testimonials,
|
|
573
|
+
faq,
|
|
574
|
+
team,
|
|
575
|
+
menu,
|
|
576
|
+
products,
|
|
577
|
+
announcements
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
function fetchInlineData(config) {
|
|
581
|
+
return config.data;
|
|
582
|
+
}
|
|
583
|
+
async function fetchSheetData(config, options = {}) {
|
|
584
|
+
switch (config.mode) {
|
|
585
|
+
case "public_csv":
|
|
586
|
+
return fetchPublicSheetData(config, options);
|
|
587
|
+
case "private_api":
|
|
588
|
+
return fetchPrivateSheetData(config, options);
|
|
589
|
+
case "inline":
|
|
590
|
+
return fetchInlineData(config);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function parseBusinessRows(rows) {
|
|
594
|
+
const data = {};
|
|
595
|
+
for (const row of rows) {
|
|
596
|
+
const key = row.key?.toLowerCase().replace(/\s+/g, "_");
|
|
597
|
+
const value = row.value;
|
|
598
|
+
if (key && value) {
|
|
599
|
+
data[key] = value;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
name: data.name || data.business_name || "",
|
|
604
|
+
tagline: data.tagline,
|
|
605
|
+
description: data.description || data.about,
|
|
606
|
+
aboutShort: data.about_short,
|
|
607
|
+
aboutLong: data.about_long,
|
|
608
|
+
phone: data.phone,
|
|
609
|
+
phoneAlt: data.phone_alt || data.phone_secondary,
|
|
610
|
+
email: data.email,
|
|
611
|
+
addressLine1: data.address_line1 || data.address || data.street,
|
|
612
|
+
addressLine2: data.address_line2,
|
|
613
|
+
city: data.city,
|
|
614
|
+
state: data.state,
|
|
615
|
+
zip: data.zip || data.zipcode || data.postal_code,
|
|
616
|
+
country: data.country || "USA",
|
|
617
|
+
googleMapsUrl: data.google_maps_url || data.maps_url,
|
|
618
|
+
mapsEmbedUrl: data.maps_embed_url || data.embed_url,
|
|
619
|
+
timezone: data.timezone || "America/Los_Angeles",
|
|
620
|
+
logoUrl: normalizeGoogleDriveUrl(data.logo_url || data.logo),
|
|
621
|
+
heroImageUrl: normalizeGoogleDriveUrl(data.hero_image_url || data.hero_image || data.hero_photo_url),
|
|
622
|
+
ogImageUrl: normalizeGoogleDriveUrl(data.og_image_url || data.og_image),
|
|
623
|
+
socialYelp: data.social_yelp || data.yelp,
|
|
624
|
+
socialInstagram: data.social_instagram || data.instagram,
|
|
625
|
+
socialFacebook: data.social_facebook || data.facebook,
|
|
626
|
+
socialTwitter: data.social_twitter || data.twitter,
|
|
627
|
+
socialLinkedIn: data.social_linkedin || data.linkedin,
|
|
628
|
+
socialYoutube: data.social_youtube || data.youtube,
|
|
629
|
+
socialTiktok: data.social_tiktok || data.tiktok,
|
|
630
|
+
primaryCtaText: data.primary_cta_text || data.cta_text,
|
|
631
|
+
primaryCtaUrl: data.primary_cta_url || data.cta_url,
|
|
632
|
+
secondaryCtaText: data.secondary_cta_text,
|
|
633
|
+
secondaryCtaUrl: data.secondary_cta_url,
|
|
634
|
+
bookingUrl: data.booking_url || data.booking_or_quote_url,
|
|
635
|
+
bannerEnabled: parseBoolean(data.banner_enabled),
|
|
636
|
+
bannerText: data.banner_text,
|
|
637
|
+
bannerUrl: data.banner_url,
|
|
638
|
+
priceRange: data.price_range,
|
|
639
|
+
yearEstablished: parseNumber(data.year_established),
|
|
640
|
+
licenseNumber: data.license_number
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
function parseHoursRows(rows) {
|
|
644
|
+
const dayMap = {
|
|
645
|
+
monday: "monday",
|
|
646
|
+
mon: "monday",
|
|
647
|
+
tuesday: "tuesday",
|
|
648
|
+
tue: "tuesday",
|
|
649
|
+
tues: "tuesday",
|
|
650
|
+
wednesday: "wednesday",
|
|
651
|
+
wed: "wednesday",
|
|
652
|
+
thursday: "thursday",
|
|
653
|
+
thu: "thursday",
|
|
654
|
+
thur: "thursday",
|
|
655
|
+
thurs: "thursday",
|
|
656
|
+
friday: "friday",
|
|
657
|
+
fri: "friday",
|
|
658
|
+
saturday: "saturday",
|
|
659
|
+
sat: "saturday",
|
|
660
|
+
sunday: "sunday",
|
|
661
|
+
sun: "sunday"
|
|
662
|
+
};
|
|
663
|
+
return rows.map((row) => {
|
|
664
|
+
const dayKey = (row.day || row.day_of_week || "").toLowerCase();
|
|
665
|
+
const day = dayMap[dayKey];
|
|
666
|
+
if (!day) return null;
|
|
667
|
+
return {
|
|
668
|
+
day,
|
|
669
|
+
open: row.open || row.open_time || row.opens,
|
|
670
|
+
close: row.close || row.close_time || row.closes,
|
|
671
|
+
closed: parseBoolean(row.closed)
|
|
672
|
+
};
|
|
673
|
+
}).filter((entry) => entry !== null);
|
|
674
|
+
}
|
|
675
|
+
function parseServicesRows(rows) {
|
|
676
|
+
return rows.map((row, index) => {
|
|
677
|
+
const title = row.title || row.name || row.service;
|
|
678
|
+
if (!title) return null;
|
|
679
|
+
return {
|
|
680
|
+
id: row.id || `service-${index}`,
|
|
681
|
+
title,
|
|
682
|
+
description: row.description || row.desc,
|
|
683
|
+
priceNote: row.price_note || row.price_hint || row.pricing,
|
|
684
|
+
price: parseNumber(row.price),
|
|
685
|
+
duration: row.duration || row.time,
|
|
686
|
+
icon: row.icon,
|
|
687
|
+
imageUrl: normalizeGoogleDriveUrl(row.image_url || row.image),
|
|
688
|
+
featured: parseBoolean(row.featured),
|
|
689
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort),
|
|
690
|
+
category: row.category
|
|
691
|
+
};
|
|
692
|
+
}).filter((service) => service !== null).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
693
|
+
}
|
|
694
|
+
function parseGalleryRows(rows) {
|
|
695
|
+
return rows.map((row, index) => {
|
|
696
|
+
const imageUrl = normalizeGoogleDriveUrl(row.image_url || row.url || row.image);
|
|
697
|
+
if (!imageUrl) return null;
|
|
698
|
+
return {
|
|
699
|
+
id: row.id || `gallery-${index}`,
|
|
700
|
+
imageUrl,
|
|
701
|
+
alt: row.alt || row.alt_text || row.description || "",
|
|
702
|
+
caption: row.caption,
|
|
703
|
+
featured: parseBoolean(row.featured),
|
|
704
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort),
|
|
705
|
+
category: row.category
|
|
706
|
+
};
|
|
707
|
+
}).filter((item) => item !== null).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
708
|
+
}
|
|
709
|
+
function parseTestimonialsRows(rows) {
|
|
710
|
+
return rows.map((row, index) => {
|
|
711
|
+
const quote = row.quote || row.text || row.review || row.testimonial;
|
|
712
|
+
const name = row.name || row.author || row.reviewer;
|
|
713
|
+
if (!quote || !name) return null;
|
|
714
|
+
return {
|
|
715
|
+
id: row.id || `testimonial-${index}`,
|
|
716
|
+
quote,
|
|
717
|
+
name,
|
|
718
|
+
context: row.context || row.service,
|
|
719
|
+
rating: parseNumber(row.rating),
|
|
720
|
+
source: row.source || row.source_label || row.platform,
|
|
721
|
+
date: row.date,
|
|
722
|
+
imageUrl: normalizeGoogleDriveUrl(row.image_url || row.photo),
|
|
723
|
+
featured: parseBoolean(row.featured),
|
|
724
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort)
|
|
725
|
+
};
|
|
726
|
+
}).filter((item) => item !== null).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
727
|
+
}
|
|
728
|
+
function parseFAQRows(rows) {
|
|
729
|
+
return rows.map((row, index) => {
|
|
730
|
+
const question = row.question || row.q;
|
|
731
|
+
const answer = row.answer || row.a;
|
|
732
|
+
if (!question || !answer) return null;
|
|
733
|
+
return {
|
|
734
|
+
id: row.id || `faq-${index}`,
|
|
735
|
+
question,
|
|
736
|
+
answer,
|
|
737
|
+
category: row.category,
|
|
738
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort)
|
|
739
|
+
};
|
|
740
|
+
}).filter((item) => item !== null).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
741
|
+
}
|
|
742
|
+
function parseTeamRows(rows) {
|
|
743
|
+
return rows.map((row, index) => {
|
|
744
|
+
const name = row.name;
|
|
745
|
+
if (!name) return null;
|
|
746
|
+
return {
|
|
747
|
+
id: row.id || `team-${index}`,
|
|
748
|
+
name,
|
|
749
|
+
role: row.role || row.title || row.position,
|
|
750
|
+
bio: row.bio || row.description,
|
|
751
|
+
imageUrl: normalizeGoogleDriveUrl(row.image_url || row.photo || row.image),
|
|
752
|
+
email: row.email,
|
|
753
|
+
phone: row.phone,
|
|
754
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort)
|
|
755
|
+
};
|
|
756
|
+
}).filter((item) => item !== null).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
757
|
+
}
|
|
758
|
+
function parseMenuRows(rows) {
|
|
759
|
+
return rows.map((row, index) => {
|
|
760
|
+
const name = row.name || row.item || row.title;
|
|
761
|
+
if (!name) return null;
|
|
762
|
+
const dietary = row.dietary || row.tags || row.dietary_tags;
|
|
763
|
+
return {
|
|
764
|
+
id: row.id || `menu-${index}`,
|
|
765
|
+
name,
|
|
766
|
+
description: row.description || row.desc,
|
|
767
|
+
price: parseNumber(row.price),
|
|
768
|
+
priceNote: row.price_note || row.pricing,
|
|
769
|
+
category: row.category || row.section,
|
|
770
|
+
imageUrl: normalizeGoogleDriveUrl(row.image_url || row.image),
|
|
771
|
+
dietary: dietary ? dietary.split(",").map((s) => s.trim()) : void 0,
|
|
772
|
+
featured: parseBoolean(row.featured),
|
|
773
|
+
available: row.available !== void 0 ? parseBoolean(row.available) : true,
|
|
774
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort)
|
|
775
|
+
};
|
|
776
|
+
}).filter((item) => item !== null).sort((a, b) => a.sortOrder - b.sortOrder);
|
|
777
|
+
}
|
|
778
|
+
function parseProductsRows(rows) {
|
|
779
|
+
const products = rows.map((row, index) => {
|
|
780
|
+
const name = row.name || row.product || row.title;
|
|
781
|
+
if (!name) return null;
|
|
782
|
+
const images = row.images || row.additional_images;
|
|
783
|
+
const product = {
|
|
784
|
+
name,
|
|
785
|
+
featured: parseBoolean(row.featured),
|
|
786
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort),
|
|
787
|
+
inStock: row.in_stock !== void 0 ? parseBoolean(row.in_stock) : true
|
|
788
|
+
};
|
|
789
|
+
if (row.id) product.id = row.id;
|
|
790
|
+
else product.id = `product-${index}`;
|
|
791
|
+
if (row.description || row.desc) product.description = row.description || row.desc;
|
|
792
|
+
if (parseNumber(row.price) !== void 0) product.price = parseNumber(row.price);
|
|
793
|
+
if (row.price_note || row.pricing) product.priceNote = row.price_note || row.pricing;
|
|
794
|
+
if (row.category) product.category = row.category;
|
|
795
|
+
const imageUrl = normalizeGoogleDriveUrl(row.image_url || row.image);
|
|
796
|
+
if (imageUrl) product.imageUrl = imageUrl;
|
|
797
|
+
if (images) {
|
|
798
|
+
product.images = images.split(",").map((s) => normalizeGoogleDriveUrl(s.trim())).filter(Boolean);
|
|
799
|
+
}
|
|
800
|
+
if (row.sku) product.sku = row.sku;
|
|
801
|
+
if (row.purchase_url || row.buy_url || row.shop_url) {
|
|
802
|
+
product.purchaseUrl = row.purchase_url || row.buy_url || row.shop_url;
|
|
803
|
+
}
|
|
804
|
+
return product;
|
|
805
|
+
}).filter((item) => item !== null);
|
|
806
|
+
return products.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
807
|
+
}
|
|
808
|
+
function parseAnnouncementsRows(rows) {
|
|
809
|
+
const announcements = rows.map((row, index) => {
|
|
810
|
+
const title = row.title || row.headline;
|
|
811
|
+
if (!title) return null;
|
|
812
|
+
const announcement = {
|
|
813
|
+
title,
|
|
814
|
+
sortOrder: parseSortOrder(row.sort_order || row.sort),
|
|
815
|
+
active: row.active !== void 0 ? parseBoolean(row.active) : true
|
|
816
|
+
};
|
|
817
|
+
if (row.id) announcement.id = row.id;
|
|
818
|
+
else announcement.id = `announcement-${index}`;
|
|
819
|
+
if (row.content || row.description || row.body) {
|
|
820
|
+
announcement.content = row.content || row.description || row.body;
|
|
821
|
+
}
|
|
822
|
+
const imageUrl = normalizeGoogleDriveUrl(row.image_url || row.image);
|
|
823
|
+
if (imageUrl) announcement.imageUrl = imageUrl;
|
|
824
|
+
if (row.link_url || row.url || row.link) {
|
|
825
|
+
announcement.linkUrl = row.link_url || row.url || row.link;
|
|
826
|
+
}
|
|
827
|
+
if (row.link_text || row.cta) announcement.linkText = row.link_text || row.cta;
|
|
828
|
+
if (row.start_date) announcement.startDate = row.start_date;
|
|
829
|
+
if (row.end_date) announcement.endDate = row.end_date;
|
|
830
|
+
return announcement;
|
|
831
|
+
}).filter((item) => item !== null);
|
|
832
|
+
return announcements.sort((a, b) => a.sortOrder - b.sortOrder);
|
|
833
|
+
}
|
|
834
|
+
function mergeSiteData(partial, defaults) {
|
|
835
|
+
return {
|
|
836
|
+
business: {
|
|
837
|
+
...defaults.business,
|
|
838
|
+
...partial.business
|
|
839
|
+
},
|
|
840
|
+
hours: partial.hours ?? defaults.hours,
|
|
841
|
+
services: partial.services ?? defaults.services,
|
|
842
|
+
gallery: partial.gallery ?? defaults.gallery,
|
|
843
|
+
testimonials: partial.testimonials ?? defaults.testimonials,
|
|
844
|
+
faq: partial.faq ?? defaults.faq,
|
|
845
|
+
team: partial.team ?? defaults.team,
|
|
846
|
+
menu: partial.menu ?? defaults.menu,
|
|
847
|
+
products: partial.products ?? defaults.products,
|
|
848
|
+
announcements: partial.announcements ?? defaults.announcements
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/data/defaults.ts
|
|
853
|
+
function createDefaultSiteData(businessName = "Your Business") {
|
|
854
|
+
return {
|
|
855
|
+
business: {
|
|
856
|
+
name: businessName,
|
|
857
|
+
tagline: "Quality Service You Can Trust",
|
|
858
|
+
description: `Welcome to ${businessName}. We are dedicated to providing exceptional service to our community.`,
|
|
859
|
+
aboutShort: `${businessName} has been serving our community with dedication and excellence.`,
|
|
860
|
+
aboutLong: `${businessName} was founded with a simple mission: to provide outstanding service with a personal touch. Our team of experienced professionals is committed to exceeding your expectations every time.`,
|
|
861
|
+
phone: "(555) 123-4567",
|
|
862
|
+
email: "info@example.com",
|
|
863
|
+
addressLine1: "123 Main Street",
|
|
864
|
+
city: "Anytown",
|
|
865
|
+
state: "CA",
|
|
866
|
+
zip: "12345",
|
|
867
|
+
country: "USA",
|
|
868
|
+
timezone: "America/Los_Angeles",
|
|
869
|
+
primaryCtaText: "Contact Us",
|
|
870
|
+
priceRange: "$$"
|
|
871
|
+
},
|
|
872
|
+
hours: [
|
|
873
|
+
{ day: "monday", open: "9:00 AM", close: "5:00 PM", closed: false },
|
|
874
|
+
{ day: "tuesday", open: "9:00 AM", close: "5:00 PM", closed: false },
|
|
875
|
+
{ day: "wednesday", open: "9:00 AM", close: "5:00 PM", closed: false },
|
|
876
|
+
{ day: "thursday", open: "9:00 AM", close: "5:00 PM", closed: false },
|
|
877
|
+
{ day: "friday", open: "9:00 AM", close: "5:00 PM", closed: false },
|
|
878
|
+
{ day: "saturday", open: "10:00 AM", close: "3:00 PM", closed: false },
|
|
879
|
+
{ day: "sunday", closed: true }
|
|
880
|
+
],
|
|
881
|
+
services: [
|
|
882
|
+
{
|
|
883
|
+
id: "service-1",
|
|
884
|
+
title: "Service One",
|
|
885
|
+
description: "Our primary service offering, designed to meet your needs.",
|
|
886
|
+
priceNote: "Starting at $50",
|
|
887
|
+
sortOrder: 1
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
id: "service-2",
|
|
891
|
+
title: "Service Two",
|
|
892
|
+
description: "Another excellent option for our valued customers.",
|
|
893
|
+
priceNote: "Call for pricing",
|
|
894
|
+
sortOrder: 2
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: "service-3",
|
|
898
|
+
title: "Service Three",
|
|
899
|
+
description: "A specialized service for unique requirements.",
|
|
900
|
+
priceNote: "Custom quote",
|
|
901
|
+
sortOrder: 3
|
|
902
|
+
}
|
|
903
|
+
],
|
|
904
|
+
gallery: [],
|
|
905
|
+
testimonials: [
|
|
906
|
+
{
|
|
907
|
+
id: "testimonial-1",
|
|
908
|
+
quote: "Excellent service! I highly recommend them to anyone looking for quality work.",
|
|
909
|
+
name: "Happy Customer",
|
|
910
|
+
rating: 5,
|
|
911
|
+
sortOrder: 1
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
id: "testimonial-2",
|
|
915
|
+
quote: "Professional, timely, and great attention to detail. Will definitely return!",
|
|
916
|
+
name: "Satisfied Client",
|
|
917
|
+
rating: 5,
|
|
918
|
+
sortOrder: 2
|
|
919
|
+
}
|
|
920
|
+
],
|
|
921
|
+
faq: [
|
|
922
|
+
{
|
|
923
|
+
id: "faq-1",
|
|
924
|
+
question: "What are your hours of operation?",
|
|
925
|
+
answer: "We are open Monday through Friday from 9 AM to 5 PM, and Saturday from 10 AM to 3 PM. We are closed on Sundays.",
|
|
926
|
+
sortOrder: 1
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
id: "faq-2",
|
|
930
|
+
question: "How can I contact you?",
|
|
931
|
+
answer: "You can reach us by phone, email, or by filling out the contact form on our website. We typically respond within 24 hours.",
|
|
932
|
+
sortOrder: 2
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
id: "faq-3",
|
|
936
|
+
question: "Do you offer free estimates?",
|
|
937
|
+
answer: "Yes! We offer free estimates for all our services. Contact us to schedule a consultation.",
|
|
938
|
+
sortOrder: 3
|
|
939
|
+
}
|
|
940
|
+
]
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
var DEFAULT_SITE_DATA = createDefaultSiteData();
|
|
944
|
+
var industryDefaults = {
|
|
945
|
+
tailor: () => ({
|
|
946
|
+
...createDefaultSiteData("Expert Tailoring"),
|
|
947
|
+
services: [
|
|
948
|
+
{ id: "s1", title: "Alterations", description: "Expert alterations for all garments", priceNote: "Starting at $15", sortOrder: 1 },
|
|
949
|
+
{ id: "s2", title: "Hemming", description: "Professional hemming for pants, skirts, and dresses", priceNote: "From $12", sortOrder: 2 },
|
|
950
|
+
{ id: "s3", title: "Wedding & Formal", description: "Specialized alterations for wedding dresses and formal wear", priceNote: "Custom quote", sortOrder: 3 },
|
|
951
|
+
{ id: "s4", title: "Custom Tailoring", description: "Made-to-measure suits and garments", priceNote: "By consultation", sortOrder: 4 }
|
|
952
|
+
],
|
|
953
|
+
faq: [
|
|
954
|
+
{ id: "f1", question: "How long do alterations take?", answer: "Most alterations are completed within 3-5 business days. Rush service is available for an additional fee.", sortOrder: 1 },
|
|
955
|
+
{ id: "f2", question: "Do you work on leather or suede?", answer: "Yes, we have experience with leather and suede garments. These require specialized care and may take longer.", sortOrder: 2 },
|
|
956
|
+
{ id: "f3", question: "Can you match fabric for repairs?", answer: "We maintain an extensive fabric library and can often find close matches. Bring your garment in for assessment.", sortOrder: 3 }
|
|
957
|
+
]
|
|
958
|
+
}),
|
|
959
|
+
restaurant: () => ({
|
|
960
|
+
...createDefaultSiteData("Local Restaurant"),
|
|
961
|
+
services: [],
|
|
962
|
+
menu: [
|
|
963
|
+
{ id: "m1", name: "Signature Dish", description: "Our house specialty", price: 18.99, category: "Entrees", sortOrder: 1 },
|
|
964
|
+
{ id: "m2", name: "Fresh Salad", description: "Garden fresh ingredients", price: 12.99, category: "Salads", sortOrder: 2 },
|
|
965
|
+
{ id: "m3", name: "Daily Special", description: "Ask your server for today's special", priceNote: "Market price", category: "Specials", sortOrder: 3 }
|
|
966
|
+
],
|
|
967
|
+
faq: [
|
|
968
|
+
{ id: "f1", question: "Do you take reservations?", answer: "Yes! We recommend reservations for parties of 4 or more, especially on weekends.", sortOrder: 1 },
|
|
969
|
+
{ id: "f2", question: "Do you offer takeout?", answer: "Yes, we offer both takeout and delivery through our website.", sortOrder: 2 },
|
|
970
|
+
{ id: "f3", question: "Do you accommodate dietary restrictions?", answer: "Absolutely! We have vegetarian, vegan, and gluten-free options. Please inform your server of any allergies.", sortOrder: 3 }
|
|
971
|
+
]
|
|
972
|
+
}),
|
|
973
|
+
salon: () => ({
|
|
974
|
+
...createDefaultSiteData("Beauty Salon"),
|
|
975
|
+
services: [
|
|
976
|
+
{ id: "s1", title: "Haircut", description: "Professional cut and style", priceNote: "From $35", sortOrder: 1 },
|
|
977
|
+
{ id: "s2", title: "Color Services", description: "Full color, highlights, and balayage", priceNote: "From $75", sortOrder: 2 },
|
|
978
|
+
{ id: "s3", title: "Styling", description: "Blowouts and special occasion styling", priceNote: "From $45", sortOrder: 3 },
|
|
979
|
+
{ id: "s4", title: "Treatments", description: "Deep conditioning and keratin treatments", priceNote: "From $50", sortOrder: 4 }
|
|
980
|
+
],
|
|
981
|
+
faq: [
|
|
982
|
+
{ id: "f1", question: "Do I need an appointment?", answer: "Appointments are recommended but we do accept walk-ins based on availability.", sortOrder: 1 },
|
|
983
|
+
{ id: "f2", question: "What is your cancellation policy?", answer: "We require 24-hour notice for cancellations to avoid a cancellation fee.", sortOrder: 2 },
|
|
984
|
+
{ id: "f3", question: "Do you offer consultations?", answer: "Yes! Free consultations are available for color services and major transformations.", sortOrder: 3 }
|
|
985
|
+
]
|
|
986
|
+
}),
|
|
987
|
+
homeKitchen: () => ({
|
|
988
|
+
...createDefaultSiteData("Home Kitchen"),
|
|
989
|
+
services: [],
|
|
990
|
+
products: [
|
|
991
|
+
{ id: "p1", name: "Fresh Baked Goods", description: "Made fresh daily", priceNote: "Varies", category: "Baked Goods", sortOrder: 1 },
|
|
992
|
+
{ id: "p2", name: "Preserves & Jams", description: "Homemade with local ingredients", priceNote: "From $8", category: "Preserves", sortOrder: 2 },
|
|
993
|
+
{ id: "p3", name: "Custom Orders", description: "Special occasion cakes and treats", priceNote: "By quote", category: "Custom", sortOrder: 3 }
|
|
994
|
+
],
|
|
995
|
+
faq: [
|
|
996
|
+
{ id: "f1", question: "Do you accommodate dietary restrictions?", answer: "Yes! We offer gluten-free, vegan, and sugar-free options. Please inquire about specific needs.", sortOrder: 1 },
|
|
997
|
+
{ id: "f2", question: "How far in advance should I order?", answer: "For custom orders, we recommend at least 1 week notice. 2 weeks for large orders.", sortOrder: 2 },
|
|
998
|
+
{ id: "f3", question: "Do you deliver?", answer: "Local delivery is available for orders over $25. Contact us for delivery area details.", sortOrder: 3 }
|
|
999
|
+
]
|
|
1000
|
+
}),
|
|
1001
|
+
repair: () => ({
|
|
1002
|
+
...createDefaultSiteData("Repair Shop"),
|
|
1003
|
+
services: [
|
|
1004
|
+
{ id: "s1", title: "Basic Repair", description: "Standard repairs and fixes", priceNote: "From $20", sortOrder: 1 },
|
|
1005
|
+
{ id: "s2", title: "Restoration", description: "Full restoration services", priceNote: "By quote", sortOrder: 2 },
|
|
1006
|
+
{ id: "s3", title: "Rush Service", description: "Same-day or next-day service", priceNote: "+50%", sortOrder: 3 },
|
|
1007
|
+
{ id: "s4", title: "Custom Work", description: "Specialized and custom requests", priceNote: "By consultation", sortOrder: 4 }
|
|
1008
|
+
],
|
|
1009
|
+
faq: [
|
|
1010
|
+
{ id: "f1", question: "How long do repairs take?", answer: "Standard repairs typically take 3-5 business days. Rush service is available.", sortOrder: 1 },
|
|
1011
|
+
{ id: "f2", question: "Do you offer a warranty?", answer: "Yes, all repairs come with a 90-day warranty against workmanship defects.", sortOrder: 2 },
|
|
1012
|
+
{ id: "f3", question: "Do you provide estimates?", answer: "Yes, we provide free estimates before beginning any work.", sortOrder: 3 }
|
|
1013
|
+
]
|
|
1014
|
+
}),
|
|
1015
|
+
professional: () => ({
|
|
1016
|
+
...createDefaultSiteData("Professional Services"),
|
|
1017
|
+
services: [
|
|
1018
|
+
{ id: "s1", title: "Consultation", description: "Initial consultation and assessment", priceNote: "Free", sortOrder: 1 },
|
|
1019
|
+
{ id: "s2", title: "Standard Service", description: "Our core service offering", priceNote: "From $100", sortOrder: 2 },
|
|
1020
|
+
{ id: "s3", title: "Premium Package", description: "Comprehensive service with priority support", priceNote: "From $250", sortOrder: 3 },
|
|
1021
|
+
{ id: "s4", title: "Ongoing Support", description: "Monthly retainer for continuous support", priceNote: "Monthly", sortOrder: 4 }
|
|
1022
|
+
],
|
|
1023
|
+
faq: [
|
|
1024
|
+
{ id: "f1", question: "What is your availability?", answer: "We maintain flexible hours to accommodate our clients. Contact us to schedule an appointment.", sortOrder: 1 },
|
|
1025
|
+
{ id: "f2", question: "Do you offer remote services?", answer: "Yes, many of our services can be provided remotely via video conference.", sortOrder: 2 },
|
|
1026
|
+
{ id: "f3", question: "What forms of payment do you accept?", answer: "We accept all major credit cards, checks, and bank transfers.", sortOrder: 3 }
|
|
1027
|
+
]
|
|
1028
|
+
})
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
// src/data/index.ts
|
|
1032
|
+
var cachedSiteData = null;
|
|
1033
|
+
var cacheTimestamp = 0;
|
|
1034
|
+
var CACHE_DURATION = 5 * 60 * 1e3;
|
|
1035
|
+
function clearSiteDataCache() {
|
|
1036
|
+
cachedSiteData = null;
|
|
1037
|
+
cacheTimestamp = 0;
|
|
1038
|
+
}
|
|
1039
|
+
async function getSiteData(config, defaults = DEFAULT_SITE_DATA) {
|
|
1040
|
+
const now = Date.now();
|
|
1041
|
+
if (cachedSiteData && now - cacheTimestamp < CACHE_DURATION) {
|
|
1042
|
+
return cachedSiteData;
|
|
1043
|
+
}
|
|
1044
|
+
if (!config) {
|
|
1045
|
+
return defaults;
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
const partialData = await fetchSheetData(config);
|
|
1049
|
+
const siteData = mergeSiteData(partialData, defaults);
|
|
1050
|
+
cachedSiteData = siteData;
|
|
1051
|
+
cacheTimestamp = now;
|
|
1052
|
+
return siteData;
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
console.error("Error fetching site data:", error);
|
|
1055
|
+
return defaults;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
async function getSiteDataWithName(config, businessName) {
|
|
1059
|
+
const defaults = createDefaultSiteData(businessName);
|
|
1060
|
+
return getSiteData(config, defaults);
|
|
1061
|
+
}
|
|
1062
|
+
function createSheetConfigFromEnv() {
|
|
1063
|
+
const mode = process.env.SHEET_MODE;
|
|
1064
|
+
if (mode === "public_csv") {
|
|
1065
|
+
const businessUrl = process.env.SHEET_CSV_URL_BUSINESS;
|
|
1066
|
+
if (!businessUrl) {
|
|
1067
|
+
console.warn("SHEET_CSV_URL_BUSINESS not set, using defaults");
|
|
1068
|
+
return void 0;
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
mode: "public_csv",
|
|
1072
|
+
tabs: {
|
|
1073
|
+
business: businessUrl,
|
|
1074
|
+
hours: process.env.SHEET_CSV_URL_HOURS,
|
|
1075
|
+
services: process.env.SHEET_CSV_URL_SERVICES,
|
|
1076
|
+
gallery: process.env.SHEET_CSV_URL_GALLERY,
|
|
1077
|
+
testimonials: process.env.SHEET_CSV_URL_TESTIMONIALS,
|
|
1078
|
+
faq: process.env.SHEET_CSV_URL_FAQ,
|
|
1079
|
+
team: process.env.SHEET_CSV_URL_TEAM,
|
|
1080
|
+
menu: process.env.SHEET_CSV_URL_MENU,
|
|
1081
|
+
products: process.env.SHEET_CSV_URL_PRODUCTS,
|
|
1082
|
+
announcements: process.env.SHEET_CSV_URL_ANNOUNCEMENTS
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (mode === "private_api") {
|
|
1087
|
+
const sheetId = process.env.SHEET_ID;
|
|
1088
|
+
const clientEmail = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
|
|
1089
|
+
const privateKey = process.env.GOOGLE_PRIVATE_KEY;
|
|
1090
|
+
if (!sheetId || !clientEmail || !privateKey) {
|
|
1091
|
+
console.warn("Missing required environment variables for private API mode");
|
|
1092
|
+
return void 0;
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
mode: "private_api",
|
|
1096
|
+
sheetId,
|
|
1097
|
+
clientEmail,
|
|
1098
|
+
privateKey
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return void 0;
|
|
1102
|
+
}
|
|
1103
|
+
function createInlineConfig(data) {
|
|
1104
|
+
return {
|
|
1105
|
+
mode: "inline",
|
|
1106
|
+
data
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function parseSimpleHours(hours) {
|
|
1110
|
+
const entries = [];
|
|
1111
|
+
const weekdays = ["monday", "tuesday", "wednesday", "thursday", "friday"];
|
|
1112
|
+
if (hours.weekdays) {
|
|
1113
|
+
for (const day of weekdays) {
|
|
1114
|
+
entries.push({
|
|
1115
|
+
day,
|
|
1116
|
+
open: hours.weekdays.open,
|
|
1117
|
+
close: hours.weekdays.close,
|
|
1118
|
+
closed: false
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
if (hours.saturday) {
|
|
1123
|
+
entries.push(
|
|
1124
|
+
hours.saturday === "closed" ? { day: "saturday", closed: true } : { day: "saturday", open: hours.saturday.open, close: hours.saturday.close, closed: false }
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (hours.sunday) {
|
|
1128
|
+
entries.push(
|
|
1129
|
+
hours.sunday === "closed" ? { day: "sunday", closed: true } : { day: "sunday", open: hours.sunday.open, close: hours.sunday.close, closed: false }
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return entries;
|
|
1133
|
+
}
|
|
1134
|
+
function parseAddressString(address) {
|
|
1135
|
+
const parts = address.split(",").map((s) => s.trim());
|
|
1136
|
+
if (parts.length >= 3) {
|
|
1137
|
+
const lastPart = parts[parts.length - 1];
|
|
1138
|
+
const stateZipMatch = lastPart.match(/^([A-Z]{2})\s*(\d{5}(?:-\d{4})?)$/);
|
|
1139
|
+
if (stateZipMatch) {
|
|
1140
|
+
return {
|
|
1141
|
+
addressLine1: parts.slice(0, -2).join(", "),
|
|
1142
|
+
city: parts[parts.length - 2],
|
|
1143
|
+
state: stateZipMatch[1],
|
|
1144
|
+
zip: stateZipMatch[2]
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (parts.length === 2) {
|
|
1149
|
+
const cityStateZipMatch = parts[1].match(/^(.+?)\s+([A-Z]{2})\s*(\d{5}(?:-\d{4})?)$/);
|
|
1150
|
+
if (cityStateZipMatch) {
|
|
1151
|
+
return {
|
|
1152
|
+
addressLine1: parts[0],
|
|
1153
|
+
city: cityStateZipMatch[1],
|
|
1154
|
+
state: cityStateZipMatch[2],
|
|
1155
|
+
zip: cityStateZipMatch[3]
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return { addressLine1: address };
|
|
1160
|
+
}
|
|
1161
|
+
function createSiteDataFromLead(lead) {
|
|
1162
|
+
const addressParts = lead.address && !lead.addressLine1 ? parseAddressString(lead.address) : {};
|
|
1163
|
+
const business = {
|
|
1164
|
+
name: lead.name,
|
|
1165
|
+
tagline: lead.tagline,
|
|
1166
|
+
description: lead.description,
|
|
1167
|
+
phone: lead.phone,
|
|
1168
|
+
phoneAlt: lead.phoneAlt,
|
|
1169
|
+
email: lead.email,
|
|
1170
|
+
addressLine1: lead.addressLine1 || addressParts.addressLine1,
|
|
1171
|
+
addressLine2: lead.addressLine2,
|
|
1172
|
+
city: lead.city || addressParts.city,
|
|
1173
|
+
state: lead.state || addressParts.state,
|
|
1174
|
+
zip: lead.zip || addressParts.zip,
|
|
1175
|
+
country: lead.country || "USA",
|
|
1176
|
+
googleMapsUrl: lead.googleMapsUrl,
|
|
1177
|
+
timezone: lead.timezone || "America/Los_Angeles",
|
|
1178
|
+
heroImageUrl: lead.heroImageUrl,
|
|
1179
|
+
logoUrl: lead.logoUrl,
|
|
1180
|
+
socialYelp: lead.yelp,
|
|
1181
|
+
socialInstagram: lead.instagram,
|
|
1182
|
+
socialFacebook: lead.facebook,
|
|
1183
|
+
socialTwitter: lead.twitter,
|
|
1184
|
+
yearEstablished: lead.yearEstablished,
|
|
1185
|
+
priceRange: lead.priceRange
|
|
1186
|
+
};
|
|
1187
|
+
let hours;
|
|
1188
|
+
if (lead.hours) {
|
|
1189
|
+
if (Array.isArray(lead.hours)) {
|
|
1190
|
+
hours = lead.hours;
|
|
1191
|
+
} else {
|
|
1192
|
+
hours = parseSimpleHours(lead.hours);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
const gallery = lead.galleryUrls?.map((url, i) => ({
|
|
1196
|
+
id: `gallery-${i}`,
|
|
1197
|
+
imageUrl: url,
|
|
1198
|
+
alt: `${lead.name} - Image ${i + 1}`,
|
|
1199
|
+
featured: false,
|
|
1200
|
+
sortOrder: i
|
|
1201
|
+
}));
|
|
1202
|
+
const testimonials = lead.testimonialQuote && lead.testimonialAuthor ? [
|
|
1203
|
+
{
|
|
1204
|
+
id: "testimonial-1",
|
|
1205
|
+
quote: lead.testimonialQuote,
|
|
1206
|
+
name: lead.testimonialAuthor,
|
|
1207
|
+
rating: lead.rating,
|
|
1208
|
+
source: "Google",
|
|
1209
|
+
featured: false,
|
|
1210
|
+
sortOrder: 0
|
|
1211
|
+
}
|
|
1212
|
+
] : lead.rating ? [
|
|
1213
|
+
{
|
|
1214
|
+
id: "testimonial-1",
|
|
1215
|
+
quote: `Rated ${lead.rating}/5 on Google${lead.reviewCount ? ` based on ${lead.reviewCount} reviews` : ""}`,
|
|
1216
|
+
name: "Google Reviews",
|
|
1217
|
+
rating: Math.round(lead.rating),
|
|
1218
|
+
source: "Google",
|
|
1219
|
+
featured: false,
|
|
1220
|
+
sortOrder: 0
|
|
1221
|
+
}
|
|
1222
|
+
] : void 0;
|
|
1223
|
+
return {
|
|
1224
|
+
business,
|
|
1225
|
+
hours,
|
|
1226
|
+
gallery,
|
|
1227
|
+
testimonials
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
async function createSiteFromLead(lead, businessType) {
|
|
1231
|
+
const partialData = createSiteDataFromLead(lead);
|
|
1232
|
+
const config = createInlineConfig(partialData);
|
|
1233
|
+
const type = businessType || lead.category?.toLowerCase().replace(/\s+/g, "-");
|
|
1234
|
+
let defaults = DEFAULT_SITE_DATA;
|
|
1235
|
+
if (type) {
|
|
1236
|
+
const typeKey = type;
|
|
1237
|
+
if (typeKey in industryDefaults) {
|
|
1238
|
+
defaults = industryDefaults[typeKey]();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
return getSiteData(config, defaults);
|
|
1242
|
+
}
|
|
1243
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1244
|
+
0 && (module.exports = {
|
|
1245
|
+
AnnouncementSchema,
|
|
1246
|
+
AnnouncementsSchema,
|
|
1247
|
+
BusinessInfoSchema,
|
|
1248
|
+
DEFAULT_SITE_DATA,
|
|
1249
|
+
DayOfWeek,
|
|
1250
|
+
FAQItemSchema,
|
|
1251
|
+
FAQSchema,
|
|
1252
|
+
GalleryItemSchema,
|
|
1253
|
+
GallerySchema,
|
|
1254
|
+
HoursEntrySchema,
|
|
1255
|
+
HoursSchema,
|
|
1256
|
+
MenuItemSchema,
|
|
1257
|
+
MenuSchema,
|
|
1258
|
+
PartialSiteDataSchema,
|
|
1259
|
+
ProductSchema,
|
|
1260
|
+
ProductsSchema,
|
|
1261
|
+
ServiceSchema,
|
|
1262
|
+
ServicesSchema,
|
|
1263
|
+
SiteDataSchema,
|
|
1264
|
+
TeamMemberSchema,
|
|
1265
|
+
TeamSchema,
|
|
1266
|
+
TestimonialSchema,
|
|
1267
|
+
TestimonialsSchema,
|
|
1268
|
+
clearSiteDataCache,
|
|
1269
|
+
createDefaultSiteData,
|
|
1270
|
+
createInlineConfig,
|
|
1271
|
+
createSheetConfigFromEnv,
|
|
1272
|
+
createSiteDataFromLead,
|
|
1273
|
+
createSiteFromLead,
|
|
1274
|
+
extractGoogleDriveFileId,
|
|
1275
|
+
fetchInlineData,
|
|
1276
|
+
fetchPrivateSheetData,
|
|
1277
|
+
fetchPublicSheetData,
|
|
1278
|
+
fetchSheetData,
|
|
1279
|
+
getGoogleDriveThumbnail,
|
|
1280
|
+
getSiteData,
|
|
1281
|
+
getSiteDataWithName,
|
|
1282
|
+
industryDefaults,
|
|
1283
|
+
isGoogleDriveUrl,
|
|
1284
|
+
mapRows,
|
|
1285
|
+
mergeSiteData,
|
|
1286
|
+
normalizeGoogleDriveUrl,
|
|
1287
|
+
normalizeHeader,
|
|
1288
|
+
parseBoolean,
|
|
1289
|
+
parseCSV,
|
|
1290
|
+
parseInteger,
|
|
1291
|
+
parseNumber,
|
|
1292
|
+
parseSortOrder,
|
|
1293
|
+
processImageUrls,
|
|
1294
|
+
processImageUrlsInArray
|
|
1295
|
+
});
|
|
1296
|
+
//# sourceMappingURL=index.js.map
|