@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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default Site Data
|
|
3
|
+
*
|
|
4
|
+
* Provides fallback data when Google Sheets data is unavailable.
|
|
5
|
+
* These defaults are used during development and as graceful fallbacks.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SiteData } from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Create default site data with customizable business name.
|
|
12
|
+
*/
|
|
13
|
+
export function createDefaultSiteData(businessName: string = 'Your Business'): SiteData {
|
|
14
|
+
return {
|
|
15
|
+
business: {
|
|
16
|
+
name: businessName,
|
|
17
|
+
tagline: 'Quality Service You Can Trust',
|
|
18
|
+
description: `Welcome to ${businessName}. We are dedicated to providing exceptional service to our community.`,
|
|
19
|
+
aboutShort: `${businessName} has been serving our community with dedication and excellence.`,
|
|
20
|
+
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.`,
|
|
21
|
+
phone: '(555) 123-4567',
|
|
22
|
+
email: 'info@example.com',
|
|
23
|
+
addressLine1: '123 Main Street',
|
|
24
|
+
city: 'Anytown',
|
|
25
|
+
state: 'CA',
|
|
26
|
+
zip: '12345',
|
|
27
|
+
country: 'USA',
|
|
28
|
+
timezone: 'America/Los_Angeles',
|
|
29
|
+
primaryCtaText: 'Contact Us',
|
|
30
|
+
priceRange: '$$',
|
|
31
|
+
},
|
|
32
|
+
hours: [
|
|
33
|
+
{ day: 'monday', open: '9:00 AM', close: '5:00 PM', closed: false },
|
|
34
|
+
{ day: 'tuesday', open: '9:00 AM', close: '5:00 PM', closed: false },
|
|
35
|
+
{ day: 'wednesday', open: '9:00 AM', close: '5:00 PM', closed: false },
|
|
36
|
+
{ day: 'thursday', open: '9:00 AM', close: '5:00 PM', closed: false },
|
|
37
|
+
{ day: 'friday', open: '9:00 AM', close: '5:00 PM', closed: false },
|
|
38
|
+
{ day: 'saturday', open: '10:00 AM', close: '3:00 PM', closed: false },
|
|
39
|
+
{ day: 'sunday', closed: true },
|
|
40
|
+
],
|
|
41
|
+
services: [
|
|
42
|
+
{
|
|
43
|
+
id: 'service-1',
|
|
44
|
+
title: 'Service One',
|
|
45
|
+
description: 'Our primary service offering, designed to meet your needs.',
|
|
46
|
+
priceNote: 'Starting at $50',
|
|
47
|
+
sortOrder: 1,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'service-2',
|
|
51
|
+
title: 'Service Two',
|
|
52
|
+
description: 'Another excellent option for our valued customers.',
|
|
53
|
+
priceNote: 'Call for pricing',
|
|
54
|
+
sortOrder: 2,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'service-3',
|
|
58
|
+
title: 'Service Three',
|
|
59
|
+
description: 'A specialized service for unique requirements.',
|
|
60
|
+
priceNote: 'Custom quote',
|
|
61
|
+
sortOrder: 3,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
gallery: [],
|
|
65
|
+
testimonials: [
|
|
66
|
+
{
|
|
67
|
+
id: 'testimonial-1',
|
|
68
|
+
quote: 'Excellent service! I highly recommend them to anyone looking for quality work.',
|
|
69
|
+
name: 'Happy Customer',
|
|
70
|
+
rating: 5,
|
|
71
|
+
sortOrder: 1,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'testimonial-2',
|
|
75
|
+
quote: 'Professional, timely, and great attention to detail. Will definitely return!',
|
|
76
|
+
name: 'Satisfied Client',
|
|
77
|
+
rating: 5,
|
|
78
|
+
sortOrder: 2,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
faq: [
|
|
82
|
+
{
|
|
83
|
+
id: 'faq-1',
|
|
84
|
+
question: 'What are your hours of operation?',
|
|
85
|
+
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.',
|
|
86
|
+
sortOrder: 1,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'faq-2',
|
|
90
|
+
question: 'How can I contact you?',
|
|
91
|
+
answer: 'You can reach us by phone, email, or by filling out the contact form on our website. We typically respond within 24 hours.',
|
|
92
|
+
sortOrder: 2,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'faq-3',
|
|
96
|
+
question: 'Do you offer free estimates?',
|
|
97
|
+
answer: 'Yes! We offer free estimates for all our services. Contact us to schedule a consultation.',
|
|
98
|
+
sortOrder: 3,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default site data instance.
|
|
106
|
+
*/
|
|
107
|
+
export const DEFAULT_SITE_DATA = createDefaultSiteData();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Industry-specific default data generators.
|
|
111
|
+
*/
|
|
112
|
+
export const industryDefaults = {
|
|
113
|
+
tailor: (): SiteData => ({
|
|
114
|
+
...createDefaultSiteData('Expert Tailoring'),
|
|
115
|
+
services: [
|
|
116
|
+
{ id: 's1', title: 'Alterations', description: 'Expert alterations for all garments', priceNote: 'Starting at $15', sortOrder: 1 },
|
|
117
|
+
{ id: 's2', title: 'Hemming', description: 'Professional hemming for pants, skirts, and dresses', priceNote: 'From $12', sortOrder: 2 },
|
|
118
|
+
{ id: 's3', title: 'Wedding & Formal', description: 'Specialized alterations for wedding dresses and formal wear', priceNote: 'Custom quote', sortOrder: 3 },
|
|
119
|
+
{ id: 's4', title: 'Custom Tailoring', description: 'Made-to-measure suits and garments', priceNote: 'By consultation', sortOrder: 4 },
|
|
120
|
+
],
|
|
121
|
+
faq: [
|
|
122
|
+
{ 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 },
|
|
123
|
+
{ 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 },
|
|
124
|
+
{ 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 },
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
restaurant: (): SiteData => ({
|
|
129
|
+
...createDefaultSiteData('Local Restaurant'),
|
|
130
|
+
services: [],
|
|
131
|
+
menu: [
|
|
132
|
+
{ id: 'm1', name: 'Signature Dish', description: 'Our house specialty', price: 18.99, category: 'Entrees', sortOrder: 1 },
|
|
133
|
+
{ id: 'm2', name: 'Fresh Salad', description: 'Garden fresh ingredients', price: 12.99, category: 'Salads', sortOrder: 2 },
|
|
134
|
+
{ id: 'm3', name: 'Daily Special', description: 'Ask your server for today\'s special', priceNote: 'Market price', category: 'Specials', sortOrder: 3 },
|
|
135
|
+
],
|
|
136
|
+
faq: [
|
|
137
|
+
{ id: 'f1', question: 'Do you take reservations?', answer: 'Yes! We recommend reservations for parties of 4 or more, especially on weekends.', sortOrder: 1 },
|
|
138
|
+
{ id: 'f2', question: 'Do you offer takeout?', answer: 'Yes, we offer both takeout and delivery through our website.', sortOrder: 2 },
|
|
139
|
+
{ 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 },
|
|
140
|
+
],
|
|
141
|
+
}),
|
|
142
|
+
|
|
143
|
+
salon: (): SiteData => ({
|
|
144
|
+
...createDefaultSiteData('Beauty Salon'),
|
|
145
|
+
services: [
|
|
146
|
+
{ id: 's1', title: 'Haircut', description: 'Professional cut and style', priceNote: 'From $35', sortOrder: 1 },
|
|
147
|
+
{ id: 's2', title: 'Color Services', description: 'Full color, highlights, and balayage', priceNote: 'From $75', sortOrder: 2 },
|
|
148
|
+
{ id: 's3', title: 'Styling', description: 'Blowouts and special occasion styling', priceNote: 'From $45', sortOrder: 3 },
|
|
149
|
+
{ id: 's4', title: 'Treatments', description: 'Deep conditioning and keratin treatments', priceNote: 'From $50', sortOrder: 4 },
|
|
150
|
+
],
|
|
151
|
+
faq: [
|
|
152
|
+
{ id: 'f1', question: 'Do I need an appointment?', answer: 'Appointments are recommended but we do accept walk-ins based on availability.', sortOrder: 1 },
|
|
153
|
+
{ id: 'f2', question: 'What is your cancellation policy?', answer: 'We require 24-hour notice for cancellations to avoid a cancellation fee.', sortOrder: 2 },
|
|
154
|
+
{ id: 'f3', question: 'Do you offer consultations?', answer: 'Yes! Free consultations are available for color services and major transformations.', sortOrder: 3 },
|
|
155
|
+
],
|
|
156
|
+
}),
|
|
157
|
+
|
|
158
|
+
homeKitchen: (): SiteData => ({
|
|
159
|
+
...createDefaultSiteData('Home Kitchen'),
|
|
160
|
+
services: [],
|
|
161
|
+
products: [
|
|
162
|
+
{ id: 'p1', name: 'Fresh Baked Goods', description: 'Made fresh daily', priceNote: 'Varies', category: 'Baked Goods', sortOrder: 1 },
|
|
163
|
+
{ id: 'p2', name: 'Preserves & Jams', description: 'Homemade with local ingredients', priceNote: 'From $8', category: 'Preserves', sortOrder: 2 },
|
|
164
|
+
{ id: 'p3', name: 'Custom Orders', description: 'Special occasion cakes and treats', priceNote: 'By quote', category: 'Custom', sortOrder: 3 },
|
|
165
|
+
],
|
|
166
|
+
faq: [
|
|
167
|
+
{ 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 },
|
|
168
|
+
{ 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 },
|
|
169
|
+
{ id: 'f3', question: 'Do you deliver?', answer: 'Local delivery is available for orders over $25. Contact us for delivery area details.', sortOrder: 3 },
|
|
170
|
+
],
|
|
171
|
+
}),
|
|
172
|
+
|
|
173
|
+
repair: (): SiteData => ({
|
|
174
|
+
...createDefaultSiteData('Repair Shop'),
|
|
175
|
+
services: [
|
|
176
|
+
{ id: 's1', title: 'Basic Repair', description: 'Standard repairs and fixes', priceNote: 'From $20', sortOrder: 1 },
|
|
177
|
+
{ id: 's2', title: 'Restoration', description: 'Full restoration services', priceNote: 'By quote', sortOrder: 2 },
|
|
178
|
+
{ id: 's3', title: 'Rush Service', description: 'Same-day or next-day service', priceNote: '+50%', sortOrder: 3 },
|
|
179
|
+
{ id: 's4', title: 'Custom Work', description: 'Specialized and custom requests', priceNote: 'By consultation', sortOrder: 4 },
|
|
180
|
+
],
|
|
181
|
+
faq: [
|
|
182
|
+
{ id: 'f1', question: 'How long do repairs take?', answer: 'Standard repairs typically take 3-5 business days. Rush service is available.', sortOrder: 1 },
|
|
183
|
+
{ id: 'f2', question: 'Do you offer a warranty?', answer: 'Yes, all repairs come with a 90-day warranty against workmanship defects.', sortOrder: 2 },
|
|
184
|
+
{ id: 'f3', question: 'Do you provide estimates?', answer: 'Yes, we provide free estimates before beginning any work.', sortOrder: 3 },
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
|
|
188
|
+
professional: (): SiteData => ({
|
|
189
|
+
...createDefaultSiteData('Professional Services'),
|
|
190
|
+
services: [
|
|
191
|
+
{ id: 's1', title: 'Consultation', description: 'Initial consultation and assessment', priceNote: 'Free', sortOrder: 1 },
|
|
192
|
+
{ id: 's2', title: 'Standard Service', description: 'Our core service offering', priceNote: 'From $100', sortOrder: 2 },
|
|
193
|
+
{ id: 's3', title: 'Premium Package', description: 'Comprehensive service with priority support', priceNote: 'From $250', sortOrder: 3 },
|
|
194
|
+
{ id: 's4', title: 'Ongoing Support', description: 'Monthly retainer for continuous support', priceNote: 'Monthly', sortOrder: 4 },
|
|
195
|
+
],
|
|
196
|
+
faq: [
|
|
197
|
+
{ id: 'f1', question: 'What is your availability?', answer: 'We maintain flexible hours to accommodate our clients. Contact us to schedule an appointment.', sortOrder: 1 },
|
|
198
|
+
{ id: 'f2', question: 'Do you offer remote services?', answer: 'Yes, many of our services can be provided remotely via video conference.', sortOrder: 2 },
|
|
199
|
+
{ id: 'f3', question: 'What forms of payment do you accept?', answer: 'We accept all major credit cards, checks, and bank transfers.', sortOrder: 3 },
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Drive URL Utilities
|
|
3
|
+
*
|
|
4
|
+
* Handles conversion of various Google Drive sharing URLs to direct image URLs.
|
|
5
|
+
* This is essential because Google Drive sharing URLs don't work directly with
|
|
6
|
+
* Next.js Image component or standard <img> tags.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Convert a Google Drive sharing URL to a direct view URL.
|
|
11
|
+
*
|
|
12
|
+
* Supported input formats:
|
|
13
|
+
* - https://drive.google.com/file/d/FILE_ID/view?usp=sharing
|
|
14
|
+
* - https://drive.google.com/file/d/FILE_ID/view
|
|
15
|
+
* - https://drive.google.com/open?id=FILE_ID
|
|
16
|
+
* - https://drive.google.com/uc?id=FILE_ID
|
|
17
|
+
* - https://drive.google.com/uc?export=view&id=FILE_ID
|
|
18
|
+
*
|
|
19
|
+
* Output format:
|
|
20
|
+
* - https://drive.google.com/uc?export=view&id=FILE_ID
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeGoogleDriveUrl(url: string | undefined): string | undefined {
|
|
23
|
+
if (!url) return undefined;
|
|
24
|
+
|
|
25
|
+
// If it's not a Google Drive URL, return as-is
|
|
26
|
+
if (!url.includes('drive.google.com')) {
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Extract the file ID using various patterns
|
|
31
|
+
let fileId: string | null = null;
|
|
32
|
+
|
|
33
|
+
// Pattern 1: /file/d/FILE_ID/
|
|
34
|
+
const filePattern = /\/file\/d\/([a-zA-Z0-9_-]+)/;
|
|
35
|
+
const fileMatch = url.match(filePattern);
|
|
36
|
+
if (fileMatch) {
|
|
37
|
+
fileId = fileMatch[1];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pattern 2: ?id=FILE_ID or &id=FILE_ID
|
|
41
|
+
if (!fileId) {
|
|
42
|
+
const idPattern = /[?&]id=([a-zA-Z0-9_-]+)/;
|
|
43
|
+
const idMatch = url.match(idPattern);
|
|
44
|
+
if (idMatch) {
|
|
45
|
+
fileId = idMatch[1];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Pattern 3: /open?id=FILE_ID
|
|
50
|
+
if (!fileId) {
|
|
51
|
+
const openPattern = /\/open\?id=([a-zA-Z0-9_-]+)/;
|
|
52
|
+
const openMatch = url.match(openPattern);
|
|
53
|
+
if (openMatch) {
|
|
54
|
+
fileId = openMatch[1];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!fileId) {
|
|
59
|
+
// Could not extract file ID, return original URL
|
|
60
|
+
return url;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Return the normalized direct view URL
|
|
64
|
+
return `https://drive.google.com/uc?export=view&id=${fileId}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get a thumbnail URL for a Google Drive file.
|
|
69
|
+
*
|
|
70
|
+
* @param url - Original Google Drive URL
|
|
71
|
+
* @param size - Thumbnail size (default: 400)
|
|
72
|
+
* @returns Thumbnail URL or undefined
|
|
73
|
+
*/
|
|
74
|
+
export function getGoogleDriveThumbnail(
|
|
75
|
+
url: string | undefined,
|
|
76
|
+
size: number = 400
|
|
77
|
+
): string | undefined {
|
|
78
|
+
if (!url) return undefined;
|
|
79
|
+
|
|
80
|
+
if (!url.includes('drive.google.com')) {
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract file ID
|
|
85
|
+
const normalizedUrl = normalizeGoogleDriveUrl(url);
|
|
86
|
+
if (!normalizedUrl) return undefined;
|
|
87
|
+
|
|
88
|
+
const idMatch = normalizedUrl.match(/id=([a-zA-Z0-9_-]+)/);
|
|
89
|
+
if (!idMatch) return undefined;
|
|
90
|
+
|
|
91
|
+
return `https://drive.google.com/thumbnail?id=${idMatch[1]}&sz=w${size}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if a URL is a Google Drive URL.
|
|
96
|
+
*/
|
|
97
|
+
export function isGoogleDriveUrl(url: string | undefined): boolean {
|
|
98
|
+
if (!url) return false;
|
|
99
|
+
return url.includes('drive.google.com');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extract the file ID from a Google Drive URL.
|
|
104
|
+
*/
|
|
105
|
+
export function extractGoogleDriveFileId(url: string | undefined): string | null {
|
|
106
|
+
if (!url) return null;
|
|
107
|
+
|
|
108
|
+
// Pattern 1: /file/d/FILE_ID/
|
|
109
|
+
const filePattern = /\/file\/d\/([a-zA-Z0-9_-]+)/;
|
|
110
|
+
const fileMatch = url.match(filePattern);
|
|
111
|
+
if (fileMatch) return fileMatch[1];
|
|
112
|
+
|
|
113
|
+
// Pattern 2: ?id=FILE_ID or &id=FILE_ID
|
|
114
|
+
const idPattern = /[?&]id=([a-zA-Z0-9_-]+)/;
|
|
115
|
+
const idMatch = url.match(idPattern);
|
|
116
|
+
if (idMatch) return idMatch[1];
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Process all image URLs in an object, converting Google Drive URLs to direct URLs.
|
|
123
|
+
* This is useful for processing entire data objects.
|
|
124
|
+
*/
|
|
125
|
+
export function processImageUrls<T extends Record<string, unknown>>(
|
|
126
|
+
obj: T,
|
|
127
|
+
imageFields: string[] = ['imageUrl', 'logoUrl', 'heroImageUrl', 'ogImageUrl']
|
|
128
|
+
): T {
|
|
129
|
+
const result = { ...obj };
|
|
130
|
+
|
|
131
|
+
for (const field of imageFields) {
|
|
132
|
+
if (field in result && typeof result[field] === 'string') {
|
|
133
|
+
(result as Record<string, unknown>)[field] = normalizeGoogleDriveUrl(result[field] as string);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Process an array of objects, normalizing image URLs in each.
|
|
142
|
+
*/
|
|
143
|
+
export function processImageUrlsInArray<T extends Record<string, unknown>>(
|
|
144
|
+
arr: T[],
|
|
145
|
+
imageFields: string[] = ['imageUrl', 'image_url', 'url']
|
|
146
|
+
): T[] {
|
|
147
|
+
return arr.map((item) => processImageUrls(item, imageFields));
|
|
148
|
+
}
|