@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,202 @@
1
+ /**
2
+ * Hero Section Component
3
+ *
4
+ * A prominent hero section for the top of pages.
5
+ * Supports various layouts and customization options.
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import React from 'react';
11
+ import type { BusinessInfo } from '../../data/types';
12
+ import { PhoneIcon, MapPinIcon, ClockIcon } from '../ui/Icons';
13
+
14
+ export interface HeroProps {
15
+ business: BusinessInfo;
16
+ variant?: 'centered' | 'left' | 'split';
17
+ showOpenStatus?: boolean;
18
+ isOpen?: boolean;
19
+ todayHours?: string;
20
+ backgroundImage?: string;
21
+ overlay?: boolean;
22
+ className?: string;
23
+ }
24
+
25
+ /**
26
+ * Hero section component.
27
+ */
28
+ export function Hero({
29
+ business,
30
+ variant = 'centered',
31
+ showOpenStatus = true,
32
+ isOpen,
33
+ todayHours,
34
+ backgroundImage,
35
+ overlay = true,
36
+ className = '',
37
+ }: HeroProps) {
38
+ const bgStyle = backgroundImage
39
+ ? { backgroundImage: `url(${backgroundImage})` }
40
+ : undefined;
41
+
42
+ const handleCallClick = () => {
43
+ if (business.phone) {
44
+ window.location.href = `tel:${business.phone.replace(/\D/g, '')}`;
45
+ }
46
+ };
47
+
48
+ const handleDirectionsClick = () => {
49
+ if (business.googleMapsUrl) {
50
+ window.open(business.googleMapsUrl, '_blank');
51
+ } else {
52
+ const address = [
53
+ business.addressLine1,
54
+ business.city,
55
+ business.state,
56
+ business.zip,
57
+ ]
58
+ .filter(Boolean)
59
+ .join(', ');
60
+ window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`, '_blank');
61
+ }
62
+ };
63
+
64
+ const handlePrimaryClick = () => {
65
+ if (business.primaryCtaUrl) {
66
+ if (business.primaryCtaUrl.startsWith('http')) {
67
+ window.open(business.primaryCtaUrl, '_blank');
68
+ } else if (business.primaryCtaUrl.startsWith('mailto:')) {
69
+ window.location.href = business.primaryCtaUrl;
70
+ } else {
71
+ window.location.href = business.primaryCtaUrl;
72
+ }
73
+ } else if (business.bookingUrl) {
74
+ window.open(business.bookingUrl, '_blank');
75
+ } else if (business.email) {
76
+ window.location.href = `mailto:${business.email}`;
77
+ }
78
+ };
79
+
80
+ return (
81
+ <section
82
+ className={`
83
+ relative min-h-[500px] flex items-center
84
+ ${backgroundImage ? 'bg-cover bg-center' : 'bg-gradient-to-br from-primary-600 to-primary-800'}
85
+ ${className}
86
+ `}
87
+ style={bgStyle}
88
+ >
89
+ {/* Overlay */}
90
+ {overlay && backgroundImage && (
91
+ <div className="absolute inset-0 bg-black/50" />
92
+ )}
93
+
94
+ {/* Content */}
95
+ <div className="relative z-10 container mx-auto px-4 py-16">
96
+ <div
97
+ className={`
98
+ ${variant === 'centered' ? 'text-center max-w-3xl mx-auto' : ''}
99
+ ${variant === 'left' ? 'max-w-2xl' : ''}
100
+ ${variant === 'split' ? 'grid md:grid-cols-2 gap-8 items-center' : ''}
101
+ `}
102
+ >
103
+ <div>
104
+ {/* Open Status */}
105
+ {showOpenStatus && isOpen !== undefined && (
106
+ <div className="mb-4">
107
+ <span
108
+ className={`
109
+ inline-flex items-center px-3 py-1 rounded-full text-sm font-medium
110
+ ${isOpen ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}
111
+ `}
112
+ >
113
+ <span
114
+ className={`w-2 h-2 rounded-full mr-2 ${isOpen ? 'bg-green-500' : 'bg-red-500'}`}
115
+ />
116
+ {isOpen ? 'Open Now' : 'Closed'}
117
+ {todayHours && <span className="ml-2 opacity-75">· {todayHours}</span>}
118
+ </span>
119
+ </div>
120
+ )}
121
+
122
+ {/* Business Name */}
123
+ <h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4">
124
+ {business.name}
125
+ </h1>
126
+
127
+ {/* Tagline */}
128
+ {business.tagline && (
129
+ <p className="text-xl md:text-2xl text-white/90 mb-6">
130
+ {business.tagline}
131
+ </p>
132
+ )}
133
+
134
+ {/* Description */}
135
+ {business.aboutShort && (
136
+ <p className="text-lg text-white/80 mb-8 max-w-xl">
137
+ {business.aboutShort}
138
+ </p>
139
+ )}
140
+
141
+ {/* Trust Signals */}
142
+ <div className="flex flex-wrap justify-center gap-4 mb-8">
143
+ {['Quality Work', 'Fair Prices', 'Fast Service'].map((signal) => (
144
+ <div key={signal} className="flex items-center text-white/90">
145
+ <svg className="w-5 h-5 text-accent-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
146
+ <path
147
+ fillRule="evenodd"
148
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
149
+ clipRule="evenodd"
150
+ />
151
+ </svg>
152
+ <span className="text-sm font-medium">{signal}</span>
153
+ </div>
154
+ ))}
155
+ </div>
156
+
157
+ {/* CTA Buttons */}
158
+ <div className="flex flex-col sm:flex-row gap-4 justify-center">
159
+ {business.phone && (
160
+ <button
161
+ onClick={handleCallClick}
162
+ className="inline-flex items-center justify-center px-6 py-3 bg-white text-primary-700 font-semibold rounded-lg hover:bg-gray-100 transition-colors"
163
+ >
164
+ <PhoneIcon size={20} className="mr-2" />
165
+ Call Now
166
+ </button>
167
+ )}
168
+
169
+ {(business.addressLine1 || business.googleMapsUrl) && (
170
+ <button
171
+ onClick={handleDirectionsClick}
172
+ className="inline-flex items-center justify-center px-6 py-3 border-2 border-white text-white font-semibold rounded-lg hover:bg-white/10 transition-colors"
173
+ >
174
+ <MapPinIcon size={20} className="mr-2" />
175
+ Get Directions
176
+ </button>
177
+ )}
178
+
179
+ {(business.primaryCtaUrl || business.bookingUrl || business.email) && (
180
+ <button
181
+ onClick={handlePrimaryClick}
182
+ className="inline-flex items-center justify-center px-6 py-3 bg-accent-500 text-white font-semibold rounded-lg hover:bg-accent-600 transition-colors"
183
+ >
184
+ {business.primaryCtaText || 'Contact Us'}
185
+ </button>
186
+ )}
187
+ </div>
188
+ </div>
189
+
190
+ {/* Split layout - right side could show an image or additional content */}
191
+ {variant === 'split' && (
192
+ <div className="hidden md:block">
193
+ {/* Placeholder for hero image or additional content */}
194
+ </div>
195
+ )}
196
+ </div>
197
+ </div>
198
+ </section>
199
+ );
200
+ }
201
+
202
+ export default Hero;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Hours Section Component
3
+ *
4
+ * Displays business hours with today highlighted.
5
+ */
6
+
7
+ import React from 'react';
8
+ import type { HoursEntry, DayOfWeek } from '../../data/types';
9
+ import { ClockIcon } from '../ui/Icons';
10
+
11
+ export interface HoursProps {
12
+ hours: HoursEntry[];
13
+ title?: string;
14
+ highlightToday?: boolean;
15
+ variant?: 'card' | 'inline' | 'minimal';
16
+ timezone?: string;
17
+ className?: string;
18
+ }
19
+
20
+ const dayOrder: DayOfWeek[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
21
+
22
+ const dayNames: Record<DayOfWeek, string> = {
23
+ monday: 'Monday',
24
+ tuesday: 'Tuesday',
25
+ wednesday: 'Wednesday',
26
+ thursday: 'Thursday',
27
+ friday: 'Friday',
28
+ saturday: 'Saturday',
29
+ sunday: 'Sunday',
30
+ };
31
+
32
+ const dayAbbrev: Record<DayOfWeek, string> = {
33
+ monday: 'Mon',
34
+ tuesday: 'Tue',
35
+ wednesday: 'Wed',
36
+ thursday: 'Thu',
37
+ friday: 'Fri',
38
+ saturday: 'Sat',
39
+ sunday: 'Sun',
40
+ };
41
+
42
+ /**
43
+ * Get today's day of week.
44
+ */
45
+ function getTodayDay(timezone?: string): DayOfWeek {
46
+ const now = timezone
47
+ ? new Date(new Date().toLocaleString('en-US', { timeZone: timezone }))
48
+ : new Date();
49
+ const jsDay = now.getDay();
50
+ // JavaScript: 0 = Sunday, 1 = Monday, etc.
51
+ return dayOrder[(jsDay + 6) % 7];
52
+ }
53
+
54
+ /**
55
+ * Hours section component.
56
+ */
57
+ export function Hours({
58
+ hours,
59
+ title = 'Hours',
60
+ highlightToday = true,
61
+ variant = 'card',
62
+ timezone,
63
+ className = '',
64
+ }: HoursProps) {
65
+ const todayDay = getTodayDay(timezone);
66
+
67
+ // Sort hours by day order
68
+ const sortedHours = [...hours].sort(
69
+ (a, b) => dayOrder.indexOf(a.day) - dayOrder.indexOf(b.day)
70
+ );
71
+
72
+ if (variant === 'inline') {
73
+ return (
74
+ <div className={`flex flex-wrap gap-4 ${className}`}>
75
+ {sortedHours.map((entry) => (
76
+ <HoursInlineItem
77
+ key={entry.day}
78
+ entry={entry}
79
+ isToday={highlightToday && entry.day === todayDay}
80
+ />
81
+ ))}
82
+ </div>
83
+ );
84
+ }
85
+
86
+ if (variant === 'minimal') {
87
+ return (
88
+ <div className={`space-y-1 ${className}`}>
89
+ {sortedHours.map((entry) => (
90
+ <HoursMinimalItem
91
+ key={entry.day}
92
+ entry={entry}
93
+ isToday={highlightToday && entry.day === todayDay}
94
+ />
95
+ ))}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ // Default: card variant
101
+ return (
102
+ <div className={`bg-white rounded-lg shadow p-6 ${className}`}>
103
+ <div className="flex items-center mb-4">
104
+ <ClockIcon size={24} className="text-primary-600 mr-2" />
105
+ <h3 className="text-xl font-semibold text-gray-900">{title}</h3>
106
+ </div>
107
+ <div className="space-y-2">
108
+ {sortedHours.map((entry) => (
109
+ <HoursCardItem
110
+ key={entry.day}
111
+ entry={entry}
112
+ isToday={highlightToday && entry.day === todayDay}
113
+ />
114
+ ))}
115
+ </div>
116
+ </div>
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Card variant item.
122
+ */
123
+ function HoursCardItem({ entry, isToday }: { entry: HoursEntry; isToday: boolean }) {
124
+ return (
125
+ <div
126
+ className={`flex justify-between py-1 ${
127
+ isToday ? 'bg-primary-50 -mx-2 px-2 rounded font-medium' : ''
128
+ }`}
129
+ >
130
+ <span className={isToday ? 'text-primary-700' : 'text-gray-600'}>
131
+ {dayNames[entry.day]}
132
+ {isToday && <span className="ml-2 text-xs">(Today)</span>}
133
+ </span>
134
+ <span className={isToday ? 'text-primary-700' : 'text-gray-900'}>
135
+ {entry.closed ? 'Closed' : `${entry.open} - ${entry.close}`}
136
+ </span>
137
+ </div>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Inline variant item.
143
+ */
144
+ function HoursInlineItem({ entry, isToday }: { entry: HoursEntry; isToday: boolean }) {
145
+ return (
146
+ <div
147
+ className={`text-center px-3 py-2 rounded ${
148
+ isToday ? 'bg-primary-100 ring-2 ring-primary-500' : 'bg-gray-100'
149
+ }`}
150
+ >
151
+ <div className={`text-sm font-medium ${isToday ? 'text-primary-700' : 'text-gray-500'}`}>
152
+ {dayAbbrev[entry.day]}
153
+ </div>
154
+ <div className={`text-sm ${isToday ? 'text-primary-900' : 'text-gray-900'}`}>
155
+ {entry.closed ? 'Closed' : `${entry.open?.split(' ')[0]}-${entry.close?.split(' ')[0]}`}
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Minimal variant item.
163
+ */
164
+ function HoursMinimalItem({ entry, isToday }: { entry: HoursEntry; isToday: boolean }) {
165
+ return (
166
+ <div className={`text-sm ${isToday ? 'font-semibold text-primary-600' : 'text-gray-600'}`}>
167
+ <span className="inline-block w-20">{dayAbbrev[entry.day]}</span>
168
+ <span>{entry.closed ? 'Closed' : `${entry.open} - ${entry.close}`}</span>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Get today's hours text.
175
+ */
176
+ export function getTodayHours(hours: HoursEntry[], timezone?: string): string | null {
177
+ const todayDay = getTodayDay(timezone);
178
+ const todayHours = hours.find((h) => h.day === todayDay);
179
+
180
+ if (!todayHours) return null;
181
+ if (todayHours.closed) return 'Closed today';
182
+ return `${todayHours.open} - ${todayHours.close}`;
183
+ }
184
+
185
+ /**
186
+ * Check if currently open.
187
+ */
188
+ export function isCurrentlyOpen(hours: HoursEntry[], timezone?: string): boolean {
189
+ const todayDay = getTodayDay(timezone);
190
+ const todayHours = hours.find((h) => h.day === todayDay);
191
+
192
+ if (!todayHours || todayHours.closed) return false;
193
+ if (!todayHours.open || !todayHours.close) return false;
194
+
195
+ const now = timezone
196
+ ? new Date(new Date().toLocaleString('en-US', { timeZone: timezone }))
197
+ : new Date();
198
+
199
+ const currentMinutes = now.getHours() * 60 + now.getMinutes();
200
+ const openMinutes = parseTimeToMinutes(todayHours.open);
201
+ const closeMinutes = parseTimeToMinutes(todayHours.close);
202
+
203
+ if (openMinutes === null || closeMinutes === null) return false;
204
+
205
+ return currentMinutes >= openMinutes && currentMinutes < closeMinutes;
206
+ }
207
+
208
+ /**
209
+ * Parse time string to minutes since midnight.
210
+ */
211
+ function parseTimeToMinutes(time: string): number | null {
212
+ const match = time.match(/(\d{1,2}):?(\d{2})?\s*(AM|PM)?/i);
213
+ if (!match) return null;
214
+
215
+ let hours = parseInt(match[1], 10);
216
+ const minutes = parseInt(match[2] || '0', 10);
217
+ const period = match[3]?.toUpperCase();
218
+
219
+ if (period === 'PM' && hours !== 12) hours += 12;
220
+ if (period === 'AM' && hours === 12) hours = 0;
221
+
222
+ return hours * 60 + minutes;
223
+ }
224
+
225
+ export default Hours;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Services Section Component
3
+ *
4
+ * Displays a grid of services offered by the business.
5
+ */
6
+
7
+ import React from 'react';
8
+ import type { Service } from '../../data/types';
9
+ import { Icon } from '../ui/Icons';
10
+
11
+ export interface ServicesProps {
12
+ services: Service[];
13
+ title?: string;
14
+ subtitle?: string;
15
+ columns?: 2 | 3 | 4;
16
+ showPrices?: boolean;
17
+ showIcons?: boolean;
18
+ variant?: 'cards' | 'list' | 'minimal';
19
+ limit?: number;
20
+ className?: string;
21
+ }
22
+
23
+ /**
24
+ * Services section component.
25
+ */
26
+ export function Services({
27
+ services,
28
+ title = 'Our Services',
29
+ subtitle,
30
+ columns = 3,
31
+ showPrices = true,
32
+ showIcons = true,
33
+ variant = 'cards',
34
+ limit,
35
+ className = '',
36
+ }: ServicesProps) {
37
+ const displayedServices = limit ? services.slice(0, limit) : services;
38
+
39
+ const gridCols = {
40
+ 2: 'md:grid-cols-2',
41
+ 3: 'md:grid-cols-2 lg:grid-cols-3',
42
+ 4: 'md:grid-cols-2 lg:grid-cols-4',
43
+ };
44
+
45
+ if (variant === 'list') {
46
+ return (
47
+ <section className={`py-16 ${className}`}>
48
+ <div className="container mx-auto px-4">
49
+ <SectionHeader title={title} subtitle={subtitle} />
50
+ <div className="max-w-3xl mx-auto divide-y divide-gray-200">
51
+ {displayedServices.map((service) => (
52
+ <ServiceListItem
53
+ key={service.id || service.title}
54
+ service={service}
55
+ showPrice={showPrices}
56
+ showIcon={showIcons}
57
+ />
58
+ ))}
59
+ </div>
60
+ </div>
61
+ </section>
62
+ );
63
+ }
64
+
65
+ if (variant === 'minimal') {
66
+ return (
67
+ <section className={`py-16 ${className}`}>
68
+ <div className="container mx-auto px-4">
69
+ <SectionHeader title={title} subtitle={subtitle} />
70
+ <div className={`grid gap-6 ${gridCols[columns]}`}>
71
+ {displayedServices.map((service) => (
72
+ <ServiceMinimalCard
73
+ key={service.id || service.title}
74
+ service={service}
75
+ showPrice={showPrices}
76
+ />
77
+ ))}
78
+ </div>
79
+ </div>
80
+ </section>
81
+ );
82
+ }
83
+
84
+ // Default: cards variant
85
+ return (
86
+ <section className={`py-16 bg-gray-50 ${className}`}>
87
+ <div className="container mx-auto px-4">
88
+ <SectionHeader title={title} subtitle={subtitle} />
89
+ <div className={`grid gap-6 ${gridCols[columns]}`}>
90
+ {displayedServices.map((service) => (
91
+ <ServiceCard
92
+ key={service.id || service.title}
93
+ service={service}
94
+ showPrice={showPrices}
95
+ showIcon={showIcons}
96
+ />
97
+ ))}
98
+ </div>
99
+ </div>
100
+ </section>
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Section header.
106
+ */
107
+ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }) {
108
+ return (
109
+ <div className="text-center mb-12">
110
+ <h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
111
+ {subtitle && (
112
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
113
+ )}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Service card component.
120
+ */
121
+ function ServiceCard({
122
+ service,
123
+ showPrice,
124
+ showIcon,
125
+ }: {
126
+ service: Service;
127
+ showPrice: boolean;
128
+ showIcon: boolean;
129
+ }) {
130
+ return (
131
+ <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
132
+ {showIcon && service.icon && (
133
+ <div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mb-4">
134
+ <Icon name={service.icon} size={24} className="text-primary-600" />
135
+ </div>
136
+ )}
137
+ <h3 className="text-xl font-semibold text-gray-900 mb-2">{service.title}</h3>
138
+ {service.description && (
139
+ <p className="text-gray-600 mb-4">{service.description}</p>
140
+ )}
141
+ {showPrice && service.priceNote && (
142
+ <p className="text-primary-600 font-medium">{service.priceNote}</p>
143
+ )}
144
+ {showPrice && service.price && !service.priceNote && (
145
+ <p className="text-primary-600 font-medium">${service.price.toFixed(2)}</p>
146
+ )}
147
+ </div>
148
+ );
149
+ }
150
+
151
+ /**
152
+ * Service list item component.
153
+ */
154
+ function ServiceListItem({
155
+ service,
156
+ showPrice,
157
+ showIcon,
158
+ }: {
159
+ service: Service;
160
+ showPrice: boolean;
161
+ showIcon: boolean;
162
+ }) {
163
+ return (
164
+ <div className="py-4 flex items-start justify-between">
165
+ <div className="flex items-start">
166
+ {showIcon && service.icon && (
167
+ <div className="w-10 h-10 bg-primary-100 rounded-lg flex items-center justify-center mr-4 flex-shrink-0">
168
+ <Icon name={service.icon} size={20} className="text-primary-600" />
169
+ </div>
170
+ )}
171
+ <div>
172
+ <h3 className="text-lg font-semibold text-gray-900">{service.title}</h3>
173
+ {service.description && (
174
+ <p className="text-gray-600 text-sm mt-1">{service.description}</p>
175
+ )}
176
+ </div>
177
+ </div>
178
+ {showPrice && (service.priceNote || service.price) && (
179
+ <div className="text-right ml-4 flex-shrink-0">
180
+ <p className="text-primary-600 font-medium">
181
+ {service.priceNote || `$${service.price?.toFixed(2)}`}
182
+ </p>
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Minimal service card component.
191
+ */
192
+ function ServiceMinimalCard({
193
+ service,
194
+ showPrice,
195
+ }: {
196
+ service: Service;
197
+ showPrice: boolean;
198
+ }) {
199
+ return (
200
+ <div className="p-4 border border-gray-200 rounded-lg hover:border-primary-300 transition-colors">
201
+ <div className="flex items-center justify-between">
202
+ <h3 className="font-semibold text-gray-900">{service.title}</h3>
203
+ {showPrice && (service.priceNote || service.price) && (
204
+ <span className="text-primary-600 text-sm font-medium">
205
+ {service.priceNote || `$${service.price?.toFixed(2)}`}
206
+ </span>
207
+ )}
208
+ </div>
209
+ {service.description && (
210
+ <p className="text-gray-600 text-sm mt-2">{service.description}</p>
211
+ )}
212
+ </div>
213
+ );
214
+ }
215
+
216
+ export default Services;