@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,159 @@
1
+ /**
2
+ * Tailor / Alterations Business Preset
3
+ */
4
+
5
+ import type { BusinessPreset } from '../types';
6
+
7
+ export const tailorPreset: BusinessPreset = {
8
+ type: 'tailor',
9
+ name: 'Tailor & Alterations',
10
+ description: 'Configuration for tailoring, alterations, and custom clothing businesses',
11
+
12
+ config: {
13
+ businessType: 'tailor',
14
+ theme: {
15
+ preset: 'warm-brown',
16
+ borderRadius: 'md',
17
+ shadows: 'md',
18
+ },
19
+ pages: {
20
+ home: {
21
+ enabled: true,
22
+ sections: [
23
+ 'hero',
24
+ 'services',
25
+ 'gallery',
26
+ 'testimonials',
27
+ 'hours',
28
+ 'location',
29
+ 'faq',
30
+ ],
31
+ },
32
+ about: { enabled: true },
33
+ services: { enabled: true, title: 'Our Services' },
34
+ gallery: { enabled: true, title: 'Our Work' },
35
+ faq: { enabled: true },
36
+ contact: { enabled: true },
37
+ privacy: { enabled: true },
38
+ },
39
+ seo: {
40
+ titleTemplate: '%s | Expert Tailoring & Alterations',
41
+ keywords: ['tailor', 'alterations', 'seamstress', 'hemming', 'custom clothing', 'wedding dress alterations'],
42
+ jsonLdType: 'LocalBusiness',
43
+ },
44
+ contactForm: {
45
+ enabled: true,
46
+ fields: {
47
+ name: true,
48
+ email: true,
49
+ phone: true,
50
+ service: true,
51
+ message: true,
52
+ },
53
+ },
54
+ features: {
55
+ mobileCallBar: true,
56
+ openStatus: true,
57
+ mapEmbed: true,
58
+ socialLinks: true,
59
+ },
60
+ },
61
+
62
+ defaults: {
63
+ business: {
64
+ tagline: 'Expert Alterations & Custom Tailoring',
65
+ description: 'Professional tailoring services with attention to detail and quality craftsmanship.',
66
+ priceRange: '$$',
67
+ },
68
+ services: [
69
+ { id: 's1', title: 'Alterations', description: 'Expert alterations for all garments including pants, shirts, dresses, and jackets', priceNote: 'Starting at $15', icon: 'scissors', sortOrder: 1 },
70
+ { id: 's2', title: 'Hemming', description: 'Professional hemming for pants, skirts, dresses, and curtains', priceNote: 'From $12', icon: 'ruler', sortOrder: 2 },
71
+ { id: 's3', title: 'Wedding & Formal', description: 'Specialized alterations for wedding dresses, bridesmaids gowns, and formal wear', priceNote: 'Custom quote', icon: 'heart', sortOrder: 3 },
72
+ { id: 's4', title: 'Repairs & Restoration', description: 'Zipper replacement, button repair, seam repair, and garment restoration', priceNote: 'From $10', icon: 'wrench', sortOrder: 4 },
73
+ { id: 's5', title: 'Custom Tailoring', description: 'Made-to-measure suits, shirts, and garments tailored to your exact specifications', priceNote: 'By consultation', icon: 'sparkles', sortOrder: 5 },
74
+ { id: 's6', title: 'Leather & Specialty', description: 'Expert work on leather, suede, and delicate fabrics', priceNote: 'Call for pricing', icon: 'star', sortOrder: 6 },
75
+ ],
76
+ faq: [
77
+ { id: 'f1', question: 'How long do alterations typically take?', answer: 'Most alterations are completed within 3-5 business days. Rush service is available for an additional fee. Wedding and formal wear may require more time for multiple fittings.', sortOrder: 1 },
78
+ { id: 'f2', question: 'Do you work on leather and suede?', answer: 'Yes, we have extensive experience with leather and suede garments. These materials require specialized care and techniques, so pricing and timing may vary.', sortOrder: 2 },
79
+ { id: 'f3', question: 'What should I bring to my fitting?', answer: 'Please bring the shoes you plan to wear with the garment, and any undergarments that might affect the fit. For formal wear, bring accessories like belts or jewelry.', sortOrder: 3 },
80
+ { id: 'f4', question: 'Do you offer wedding dress alterations?', answer: 'Absolutely! We specialize in wedding dress alterations including hemming, bustling, taking in or letting out, and adding embellishments. We recommend scheduling your first fitting 2-3 months before the wedding.', sortOrder: 4 },
81
+ { id: 'f5', question: 'Can you match fabric for repairs?', answer: 'We maintain an extensive fabric library and can often find close matches for repairs and patches. Bring your garment in for a free assessment.', sortOrder: 5 },
82
+ ],
83
+ },
84
+
85
+ sheetTemplate: {
86
+ tabs: [
87
+ {
88
+ name: 'business',
89
+ required: true,
90
+ columns: [
91
+ { name: 'key', description: 'Setting name', example: 'name', required: true },
92
+ { name: 'value', description: 'Setting value', example: 'Expert Tailoring', required: true },
93
+ ],
94
+ },
95
+ {
96
+ name: 'hours',
97
+ required: true,
98
+ columns: [
99
+ { name: 'day', description: 'Day of week', example: 'Monday', required: true },
100
+ { name: 'open', description: 'Opening time', example: '9:00 AM', required: false },
101
+ { name: 'close', description: 'Closing time', example: '6:00 PM', required: false },
102
+ { name: 'closed', description: 'Is closed?', example: 'FALSE', required: false },
103
+ ],
104
+ },
105
+ {
106
+ name: 'services',
107
+ required: true,
108
+ columns: [
109
+ { name: 'title', description: 'Service name', example: 'Alterations', required: true },
110
+ { name: 'description', description: 'Service description', example: 'Expert alterations for all garments', required: false },
111
+ { name: 'price_note', description: 'Pricing info', example: 'Starting at $15', required: false },
112
+ { name: 'icon', description: 'Icon name', example: 'scissors', required: false },
113
+ { name: 'sort', description: 'Display order', example: '1', required: false },
114
+ ],
115
+ },
116
+ {
117
+ name: 'gallery',
118
+ required: false,
119
+ columns: [
120
+ { name: 'image_url', description: 'Image URL (Google Drive or direct)', example: 'https://drive.google.com/...', required: true },
121
+ { name: 'alt', description: 'Image description', example: 'Wedding dress alteration', required: false },
122
+ { name: 'caption', description: 'Caption text', example: 'Custom bridal alterations', required: false },
123
+ { name: 'sort', description: 'Display order', example: '1', required: false },
124
+ ],
125
+ },
126
+ {
127
+ name: 'testimonials',
128
+ required: false,
129
+ columns: [
130
+ { name: 'quote', description: 'Customer review', example: 'Amazing work on my wedding dress!', required: true },
131
+ { name: 'name', description: 'Customer name', example: 'Sarah M.', required: true },
132
+ { name: 'context', description: 'Service context', example: 'Wedding alterations', required: false },
133
+ { name: 'rating', description: 'Star rating (1-5)', example: '5', required: false },
134
+ { name: 'sort', description: 'Display order', example: '1', required: false },
135
+ ],
136
+ },
137
+ {
138
+ name: 'faq',
139
+ required: false,
140
+ columns: [
141
+ { name: 'question', description: 'Question text', example: 'How long do alterations take?', required: true },
142
+ { name: 'answer', description: 'Answer text', example: '3-5 business days typically', required: true },
143
+ { name: 'sort', description: 'Display order', example: '1', required: false },
144
+ ],
145
+ },
146
+ ],
147
+ },
148
+
149
+ iconSuggestions: ['scissors', 'ruler', 'heart', 'sparkles', 'star', 'wrench', 'shirt', 'dress'],
150
+
151
+ imageSuggestions: [
152
+ 'Professional tailor measuring fabric',
153
+ 'Close-up of precise stitching',
154
+ 'Wedding dress on mannequin',
155
+ 'Organized tailor workshop',
156
+ 'Before and after alterations',
157
+ 'Happy customer with altered garment',
158
+ ],
159
+ };
@@ -0,0 +1,314 @@
1
+ /**
2
+ * SheetSite Configuration Types
3
+ *
4
+ * Defines the configuration structure for creating sites.
5
+ * These types are used by both the library and the CLI generator.
6
+ */
7
+
8
+ import type { SiteData } from '../data/types';
9
+
10
+ // =============================================================================
11
+ // BUSINESS TYPES
12
+ // =============================================================================
13
+
14
+ /**
15
+ * Supported business types with specialized configurations.
16
+ */
17
+ export type BusinessType =
18
+ // Service Businesses
19
+ | 'tailor'
20
+ | 'shoe-repair'
21
+ | 'dry-cleaner'
22
+ | 'salon'
23
+ | 'barbershop'
24
+ | 'spa'
25
+ | 'nail-salon'
26
+ | 'pet-grooming'
27
+ // Food & Beverage
28
+ | 'restaurant'
29
+ | 'cafe'
30
+ | 'bakery'
31
+ | 'food-truck'
32
+ | 'home-kitchen'
33
+ | 'catering'
34
+ // Retail
35
+ | 'craft-seller'
36
+ | 'boutique'
37
+ | 'florist'
38
+ | 'gift-shop'
39
+ // Home Services
40
+ | 'cleaning'
41
+ | 'landscaping'
42
+ | 'handyman'
43
+ | 'plumber'
44
+ | 'electrician'
45
+ | 'hvac'
46
+ | 'moving'
47
+ | 'pest-control'
48
+ // Auto Services
49
+ | 'auto-repair'
50
+ | 'auto-detail'
51
+ | 'tire-shop'
52
+ // Professional Services
53
+ | 'accountant'
54
+ | 'lawyer'
55
+ | 'real-estate'
56
+ | 'insurance'
57
+ | 'consulting'
58
+ // Health & Wellness
59
+ | 'personal-trainer'
60
+ | 'yoga-studio'
61
+ | 'martial-arts'
62
+ | 'chiropractor'
63
+ | 'massage'
64
+ // Education & Creative
65
+ | 'tutoring'
66
+ | 'music-lessons'
67
+ | 'photography'
68
+ | 'videography'
69
+ | 'event-planning'
70
+ // General
71
+ | 'generic';
72
+
73
+ // =============================================================================
74
+ // THEME CONFIGURATION
75
+ // =============================================================================
76
+
77
+ /**
78
+ * Color palette configuration.
79
+ */
80
+ export interface ColorPalette {
81
+ primary: {
82
+ 50: string;
83
+ 100: string;
84
+ 200: string;
85
+ 300: string;
86
+ 400: string;
87
+ 500: string;
88
+ 600: string;
89
+ 700: string;
90
+ 800: string;
91
+ 900: string;
92
+ 950: string;
93
+ };
94
+ accent: {
95
+ 50: string;
96
+ 100: string;
97
+ 200: string;
98
+ 300: string;
99
+ 400: string;
100
+ 500: string;
101
+ 600: string;
102
+ 700: string;
103
+ 800: string;
104
+ 900: string;
105
+ 950: string;
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Font configuration.
111
+ */
112
+ export interface FontConfig {
113
+ heading: string;
114
+ body: string;
115
+ accent?: string;
116
+ }
117
+
118
+ /**
119
+ * Theme preset names.
120
+ */
121
+ export type ThemePreset =
122
+ | 'warm-brown' // Tailoring, leather, wood
123
+ | 'cool-blue' // Professional, tech, medical
124
+ | 'earth-green' // Natural, organic, outdoor
125
+ | 'warm-amber' // Food, bakery, hospitality
126
+ | 'elegant-gold' // Luxury, formal, upscale
127
+ | 'fresh-teal' // Modern, creative, spa
128
+ | 'bold-red' // Auto, sports, urgent
129
+ | 'soft-pink' // Beauty, feminine, delicate
130
+ | 'slate-gray' // Minimalist, industrial
131
+ | 'forest-green'; // Eco, outdoor, nature
132
+
133
+ /**
134
+ * Complete theme configuration.
135
+ */
136
+ export interface ThemeConfig {
137
+ preset?: ThemePreset;
138
+ colors?: Partial<ColorPalette>;
139
+ fonts?: Partial<FontConfig>;
140
+ borderRadius?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
141
+ shadows?: 'none' | 'sm' | 'md' | 'lg';
142
+ }
143
+
144
+ // =============================================================================
145
+ // PAGE CONFIGURATION
146
+ // =============================================================================
147
+
148
+ /**
149
+ * Available page types.
150
+ */
151
+ export type PageType =
152
+ | 'home'
153
+ | 'about'
154
+ | 'services'
155
+ | 'menu'
156
+ | 'products'
157
+ | 'gallery'
158
+ | 'testimonials'
159
+ | 'team'
160
+ | 'faq'
161
+ | 'contact'
162
+ | 'privacy';
163
+
164
+ /**
165
+ * Home page section configuration.
166
+ */
167
+ export type HomeSectionType =
168
+ | 'hero'
169
+ | 'services'
170
+ | 'menu'
171
+ | 'products'
172
+ | 'gallery'
173
+ | 'testimonials'
174
+ | 'team'
175
+ | 'faq'
176
+ | 'hours'
177
+ | 'location'
178
+ | 'contact-form'
179
+ | 'cta'
180
+ | 'how-it-works'
181
+ | 'announcements'
182
+ | 'about-preview';
183
+
184
+ /**
185
+ * Page configuration.
186
+ */
187
+ export interface PageConfig {
188
+ enabled: boolean;
189
+ title?: string;
190
+ sections?: HomeSectionType[];
191
+ }
192
+
193
+ // =============================================================================
194
+ // SITE CONFIGURATION
195
+ // =============================================================================
196
+
197
+ /**
198
+ * SEO configuration.
199
+ */
200
+ export interface SEOConfig {
201
+ titleTemplate?: string;
202
+ defaultDescription?: string;
203
+ keywords?: string[];
204
+ ogImage?: string;
205
+ twitterHandle?: string;
206
+ jsonLdType?: 'LocalBusiness' | 'Restaurant' | 'Store' | 'ProfessionalService' | 'HealthAndBeautyBusiness';
207
+ }
208
+
209
+ /**
210
+ * Contact form configuration.
211
+ */
212
+ export interface ContactFormConfig {
213
+ enabled: boolean;
214
+ provider?: 'resend' | 'sendgrid' | 'mailgun' | 'custom';
215
+ fields?: {
216
+ name: boolean;
217
+ email: boolean;
218
+ phone: boolean;
219
+ service: boolean;
220
+ message: boolean;
221
+ };
222
+ successMessage?: string;
223
+ }
224
+
225
+ /**
226
+ * Feature flags.
227
+ */
228
+ export interface FeatureFlags {
229
+ mobileCallBar?: boolean;
230
+ announcementBanner?: boolean;
231
+ openStatus?: boolean;
232
+ mapEmbed?: boolean;
233
+ socialLinks?: boolean;
234
+ bookingButton?: boolean;
235
+ }
236
+
237
+ /**
238
+ * Complete site configuration.
239
+ */
240
+ export interface SiteConfig {
241
+ // Basic info
242
+ businessType: BusinessType;
243
+ siteUrl?: string;
244
+
245
+ // Theme
246
+ theme: ThemeConfig;
247
+
248
+ // Pages
249
+ pages: {
250
+ home: PageConfig & { sections: HomeSectionType[] };
251
+ about?: PageConfig;
252
+ services?: PageConfig;
253
+ menu?: PageConfig;
254
+ products?: PageConfig;
255
+ gallery?: PageConfig;
256
+ testimonials?: PageConfig;
257
+ team?: PageConfig;
258
+ faq?: PageConfig;
259
+ contact?: PageConfig;
260
+ privacy?: PageConfig;
261
+ };
262
+
263
+ // SEO
264
+ seo?: SEOConfig;
265
+
266
+ // Contact
267
+ contactForm?: ContactFormConfig;
268
+
269
+ // Features
270
+ features?: FeatureFlags;
271
+
272
+ // Custom CSS classes
273
+ customClasses?: {
274
+ container?: string;
275
+ section?: string;
276
+ button?: string;
277
+ card?: string;
278
+ };
279
+ }
280
+
281
+ // =============================================================================
282
+ // BUSINESS PRESET
283
+ // =============================================================================
284
+
285
+ /**
286
+ * A complete preset for a business type.
287
+ * Includes configuration, default data, and content suggestions.
288
+ */
289
+ export interface BusinessPreset {
290
+ type: BusinessType;
291
+ name: string;
292
+ description: string;
293
+ config: SiteConfig;
294
+ defaults: Partial<SiteData>;
295
+ sheetTemplate: SheetTemplate;
296
+ iconSuggestions: string[];
297
+ imageSuggestions: string[];
298
+ }
299
+
300
+ /**
301
+ * Google Sheet template structure.
302
+ */
303
+ export interface SheetTemplate {
304
+ tabs: {
305
+ name: string;
306
+ required: boolean;
307
+ columns: {
308
+ name: string;
309
+ description: string;
310
+ example: string;
311
+ required: boolean;
312
+ }[];
313
+ }[];
314
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * CSV Parser
3
+ *
4
+ * A robust CSV parser that handles:
5
+ * - Quoted fields with embedded commas
6
+ * - Escaped quotes within quoted fields
7
+ * - Various line endings (CRLF, LF)
8
+ * - Empty values
9
+ * - Header normalization
10
+ */
11
+
12
+ export interface ParsedRow {
13
+ [key: string]: string;
14
+ }
15
+
16
+ /**
17
+ * Parse a CSV string into an array of objects.
18
+ * Headers are normalized: lowercased and spaces replaced with underscores.
19
+ */
20
+ export function parseCSV(csvText: string): ParsedRow[] {
21
+ const lines = csvText.split(/\r?\n/);
22
+ if (lines.length < 2) return [];
23
+
24
+ // Parse header row
25
+ const headers = parseCSVLine(lines[0]).map(normalizeHeader);
26
+ if (headers.length === 0) return [];
27
+
28
+ const rows: ParsedRow[] = [];
29
+
30
+ for (let i = 1; i < lines.length; i++) {
31
+ const line = lines[i].trim();
32
+ if (!line) continue;
33
+
34
+ const values = parseCSVLine(line);
35
+ const row: ParsedRow = {};
36
+
37
+ for (let j = 0; j < headers.length; j++) {
38
+ row[headers[j]] = values[j]?.trim() ?? '';
39
+ }
40
+
41
+ rows.push(row);
42
+ }
43
+
44
+ return rows;
45
+ }
46
+
47
+ /**
48
+ * Parse a single CSV line, handling quoted fields correctly.
49
+ */
50
+ function parseCSVLine(line: string): string[] {
51
+ const values: string[] = [];
52
+ let current = '';
53
+ let inQuotes = false;
54
+ let i = 0;
55
+
56
+ while (i < line.length) {
57
+ const char = line[i];
58
+ const nextChar = line[i + 1];
59
+
60
+ if (inQuotes) {
61
+ if (char === '"') {
62
+ if (nextChar === '"') {
63
+ // Escaped quote
64
+ current += '"';
65
+ i += 2;
66
+ } else {
67
+ // End of quoted field
68
+ inQuotes = false;
69
+ i++;
70
+ }
71
+ } else {
72
+ current += char;
73
+ i++;
74
+ }
75
+ } else {
76
+ if (char === '"') {
77
+ inQuotes = true;
78
+ i++;
79
+ } else if (char === ',') {
80
+ values.push(current);
81
+ current = '';
82
+ i++;
83
+ } else {
84
+ current += char;
85
+ i++;
86
+ }
87
+ }
88
+ }
89
+
90
+ // Push the last value
91
+ values.push(current);
92
+
93
+ return values;
94
+ }
95
+
96
+ /**
97
+ * Normalize a header string:
98
+ * - Convert to lowercase
99
+ * - Replace spaces with underscores
100
+ * - Remove special characters
101
+ */
102
+ export function normalizeHeader(header: string): string {
103
+ return header
104
+ .toLowerCase()
105
+ .trim()
106
+ .replace(/\s+/g, '_')
107
+ .replace(/[^a-z0-9_]/g, '');
108
+ }
109
+
110
+ /**
111
+ * Convert CSV rows to typed objects using a mapping function.
112
+ */
113
+ export function mapRows<T>(
114
+ rows: ParsedRow[],
115
+ mapper: (row: ParsedRow, index: number) => T | null
116
+ ): T[] {
117
+ return rows
118
+ .map((row, index) => mapper(row, index))
119
+ .filter((item): item is T => item !== null);
120
+ }
121
+
122
+ /**
123
+ * Parse a boolean value from CSV (handles various formats).
124
+ */
125
+ export function parseBoolean(value: string | undefined): boolean {
126
+ if (!value) return false;
127
+ const lower = value.toLowerCase().trim();
128
+ return lower === 'true' || lower === 'yes' || lower === '1';
129
+ }
130
+
131
+ /**
132
+ * Parse a number value from CSV.
133
+ */
134
+ export function parseNumber(value: string | undefined): number | undefined {
135
+ if (!value) return undefined;
136
+ const num = parseFloat(value);
137
+ return isNaN(num) ? undefined : num;
138
+ }
139
+
140
+ /**
141
+ * Parse an integer value from CSV.
142
+ */
143
+ export function parseInteger(value: string | undefined): number | undefined {
144
+ if (!value) return undefined;
145
+ const num = parseInt(value, 10);
146
+ return isNaN(num) ? undefined : num;
147
+ }
148
+
149
+ /**
150
+ * Parse a sort order, defaulting to 0.
151
+ */
152
+ export function parseSortOrder(value: string | undefined): number {
153
+ return parseInteger(value) ?? 0;
154
+ }