@aws505/sheetsite 1.0.2 → 1.0.3
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 +185 -40
- package/dist/components/index.js +804 -57
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +789 -57
- package/dist/components/index.mjs.map +1 -1
- package/dist/index.js +175 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +165 -59
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +12 -0
- package/src/components/sections/BeforeAfter.tsx +345 -0
- package/src/components/sections/FAQ.tsx +3 -3
- package/src/components/sections/Gallery.tsx +104 -4
- package/src/components/sections/Menu.tsx +312 -0
- package/src/components/sections/Services.tsx +3 -3
- package/src/components/sections/Testimonials.tsx +1 -1
- package/src/components/sections/TrustBadges.tsx +283 -0
- package/src/components/ui/AnimatedSection.tsx +136 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Menu Section Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a restaurant or food service menu with categories.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react';
|
|
10
|
+
import type { MenuItem } from '../../data/types';
|
|
11
|
+
|
|
12
|
+
export interface MenuProps {
|
|
13
|
+
items: MenuItem[];
|
|
14
|
+
title?: string;
|
|
15
|
+
subtitle?: string;
|
|
16
|
+
showCategories?: boolean;
|
|
17
|
+
showImages?: boolean;
|
|
18
|
+
showDietary?: boolean;
|
|
19
|
+
variant?: 'cards' | 'list' | 'compact';
|
|
20
|
+
columns?: 1 | 2 | 3;
|
|
21
|
+
className?: string;
|
|
22
|
+
id?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Menu section component.
|
|
27
|
+
*/
|
|
28
|
+
export function Menu({
|
|
29
|
+
items,
|
|
30
|
+
title = 'Our Menu',
|
|
31
|
+
subtitle,
|
|
32
|
+
showCategories = true,
|
|
33
|
+
showImages = true,
|
|
34
|
+
showDietary = true,
|
|
35
|
+
variant = 'cards',
|
|
36
|
+
columns = 2,
|
|
37
|
+
className = '',
|
|
38
|
+
id = 'menu',
|
|
39
|
+
}: MenuProps) {
|
|
40
|
+
// Get unique categories
|
|
41
|
+
const categories = showCategories
|
|
42
|
+
? [...new Set(items.filter(item => item.category).map(item => item.category!))]
|
|
43
|
+
: [];
|
|
44
|
+
|
|
45
|
+
const [activeCategory, setActiveCategory] = useState<string | null>(
|
|
46
|
+
categories.length > 0 ? categories[0] : null
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Filter items by category
|
|
50
|
+
const displayedItems = activeCategory
|
|
51
|
+
? items.filter(item => item.category === activeCategory)
|
|
52
|
+
: items;
|
|
53
|
+
|
|
54
|
+
// Sort by sortOrder
|
|
55
|
+
const sortedItems = [...displayedItems].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
|
56
|
+
|
|
57
|
+
const gridCols = {
|
|
58
|
+
1: 'max-w-2xl mx-auto',
|
|
59
|
+
2: 'md:grid-cols-2',
|
|
60
|
+
3: 'md:grid-cols-2 lg:grid-cols-3',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<section id={id} className={`py-16 scroll-mt-20 ${className}`}>
|
|
65
|
+
<div className="container mx-auto px-4">
|
|
66
|
+
{/* Header */}
|
|
67
|
+
<div className="text-center mb-12">
|
|
68
|
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
|
|
69
|
+
{subtitle && (
|
|
70
|
+
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Category Tabs */}
|
|
75
|
+
{showCategories && categories.length > 1 && (
|
|
76
|
+
<div className="flex flex-wrap justify-center gap-2 mb-8">
|
|
77
|
+
{categories.map((category) => (
|
|
78
|
+
<button
|
|
79
|
+
key={category}
|
|
80
|
+
onClick={() => setActiveCategory(category)}
|
|
81
|
+
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
|
82
|
+
activeCategory === category
|
|
83
|
+
? 'bg-primary-600 text-white'
|
|
84
|
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
{category}
|
|
88
|
+
</button>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{/* Menu Items */}
|
|
94
|
+
{variant === 'list' ? (
|
|
95
|
+
<div className="max-w-3xl mx-auto divide-y divide-gray-200">
|
|
96
|
+
{sortedItems.map((item) => (
|
|
97
|
+
<MenuListItem
|
|
98
|
+
key={item.id || item.name}
|
|
99
|
+
item={item}
|
|
100
|
+
showImage={showImages}
|
|
101
|
+
showDietary={showDietary}
|
|
102
|
+
/>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
) : variant === 'compact' ? (
|
|
106
|
+
<div className={`grid gap-4 ${gridCols[columns]}`}>
|
|
107
|
+
{sortedItems.map((item) => (
|
|
108
|
+
<MenuCompactItem
|
|
109
|
+
key={item.id || item.name}
|
|
110
|
+
item={item}
|
|
111
|
+
showDietary={showDietary}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
) : (
|
|
116
|
+
<div className={`grid gap-6 ${gridCols[columns]}`}>
|
|
117
|
+
{sortedItems.map((item) => (
|
|
118
|
+
<MenuCard
|
|
119
|
+
key={item.id || item.name}
|
|
120
|
+
item={item}
|
|
121
|
+
showImage={showImages}
|
|
122
|
+
showDietary={showDietary}
|
|
123
|
+
/>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</section>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Menu card component.
|
|
134
|
+
*/
|
|
135
|
+
function MenuCard({
|
|
136
|
+
item,
|
|
137
|
+
showImage,
|
|
138
|
+
showDietary,
|
|
139
|
+
}: {
|
|
140
|
+
item: MenuItem;
|
|
141
|
+
showImage: boolean;
|
|
142
|
+
showDietary: boolean;
|
|
143
|
+
}) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
|
146
|
+
{showImage && item.imageUrl && (
|
|
147
|
+
<div className="aspect-video relative overflow-hidden">
|
|
148
|
+
<img
|
|
149
|
+
src={item.imageUrl}
|
|
150
|
+
alt={item.name}
|
|
151
|
+
className="w-full h-full object-cover"
|
|
152
|
+
/>
|
|
153
|
+
{item.featured && (
|
|
154
|
+
<span className="absolute top-2 left-2 px-2 py-1 bg-primary-600 text-white text-xs font-medium rounded">
|
|
155
|
+
Featured
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
<div className="p-4">
|
|
161
|
+
<div className="flex justify-between items-start mb-2">
|
|
162
|
+
<h3 className="text-lg font-semibold text-gray-900">{item.name}</h3>
|
|
163
|
+
<PriceDisplay item={item} />
|
|
164
|
+
</div>
|
|
165
|
+
{item.description && (
|
|
166
|
+
<p className="text-gray-600 text-sm mb-3">{item.description}</p>
|
|
167
|
+
)}
|
|
168
|
+
{showDietary && item.dietary && item.dietary.length > 0 && (
|
|
169
|
+
<DietaryBadges dietary={item.dietary} />
|
|
170
|
+
)}
|
|
171
|
+
{!item.available && (
|
|
172
|
+
<span className="inline-block mt-2 px-2 py-1 bg-gray-100 text-gray-500 text-xs rounded">
|
|
173
|
+
Currently unavailable
|
|
174
|
+
</span>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Menu list item component.
|
|
183
|
+
*/
|
|
184
|
+
function MenuListItem({
|
|
185
|
+
item,
|
|
186
|
+
showImage,
|
|
187
|
+
showDietary,
|
|
188
|
+
}: {
|
|
189
|
+
item: MenuItem;
|
|
190
|
+
showImage: boolean;
|
|
191
|
+
showDietary: boolean;
|
|
192
|
+
}) {
|
|
193
|
+
return (
|
|
194
|
+
<div className="py-4 flex gap-4">
|
|
195
|
+
{showImage && item.imageUrl && (
|
|
196
|
+
<div className="w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden">
|
|
197
|
+
<img
|
|
198
|
+
src={item.imageUrl}
|
|
199
|
+
alt={item.name}
|
|
200
|
+
className="w-full h-full object-cover"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
)}
|
|
204
|
+
<div className="flex-1 min-w-0">
|
|
205
|
+
<div className="flex justify-between items-start">
|
|
206
|
+
<div>
|
|
207
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
208
|
+
{item.name}
|
|
209
|
+
{item.featured && (
|
|
210
|
+
<span className="ml-2 px-2 py-0.5 bg-primary-100 text-primary-700 text-xs font-medium rounded">
|
|
211
|
+
Popular
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
</h3>
|
|
215
|
+
{item.description && (
|
|
216
|
+
<p className="text-gray-600 text-sm mt-1">{item.description}</p>
|
|
217
|
+
)}
|
|
218
|
+
{showDietary && item.dietary && item.dietary.length > 0 && (
|
|
219
|
+
<div className="mt-2">
|
|
220
|
+
<DietaryBadges dietary={item.dietary} />
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
<PriceDisplay item={item} />
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Compact menu item component.
|
|
233
|
+
*/
|
|
234
|
+
function MenuCompactItem({
|
|
235
|
+
item,
|
|
236
|
+
showDietary,
|
|
237
|
+
}: {
|
|
238
|
+
item: MenuItem;
|
|
239
|
+
showDietary: boolean;
|
|
240
|
+
}) {
|
|
241
|
+
return (
|
|
242
|
+
<div className="flex justify-between items-center py-2 border-b border-gray-100 last:border-0">
|
|
243
|
+
<div className="flex items-center gap-2">
|
|
244
|
+
<span className="font-medium text-gray-900">{item.name}</span>
|
|
245
|
+
{item.featured && (
|
|
246
|
+
<span className="w-2 h-2 bg-primary-500 rounded-full" title="Popular" />
|
|
247
|
+
)}
|
|
248
|
+
{showDietary && item.dietary && item.dietary.length > 0 && (
|
|
249
|
+
<span className="text-xs text-gray-500">
|
|
250
|
+
({item.dietary.join(', ')})
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<PriceDisplay item={item} compact />
|
|
255
|
+
</div>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Price display component.
|
|
261
|
+
*/
|
|
262
|
+
function PriceDisplay({ item, compact = false }: { item: MenuItem; compact?: boolean }) {
|
|
263
|
+
if (item.priceNote) {
|
|
264
|
+
return (
|
|
265
|
+
<span className={`text-primary-600 font-medium ${compact ? 'text-sm' : ''}`}>
|
|
266
|
+
{item.priceNote}
|
|
267
|
+
</span>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (item.price) {
|
|
272
|
+
return (
|
|
273
|
+
<span className={`text-primary-600 font-medium ${compact ? 'text-sm' : ''}`}>
|
|
274
|
+
${item.price.toFixed(2)}
|
|
275
|
+
</span>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Dietary badges component.
|
|
284
|
+
*/
|
|
285
|
+
function DietaryBadges({ dietary }: { dietary: string[] }) {
|
|
286
|
+
const dietaryIcons: Record<string, string> = {
|
|
287
|
+
vegetarian: 'V',
|
|
288
|
+
vegan: 'VG',
|
|
289
|
+
'gluten-free': 'GF',
|
|
290
|
+
'dairy-free': 'DF',
|
|
291
|
+
'nut-free': 'NF',
|
|
292
|
+
spicy: '🌶',
|
|
293
|
+
halal: 'H',
|
|
294
|
+
kosher: 'K',
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className="flex flex-wrap gap-1">
|
|
299
|
+
{dietary.map((diet) => (
|
|
300
|
+
<span
|
|
301
|
+
key={diet}
|
|
302
|
+
className="px-1.5 py-0.5 bg-green-100 text-green-700 text-xs font-medium rounded"
|
|
303
|
+
title={diet}
|
|
304
|
+
>
|
|
305
|
+
{dietaryIcons[diet.toLowerCase()] || diet}
|
|
306
|
+
</span>
|
|
307
|
+
))}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default Menu;
|
|
@@ -46,7 +46,7 @@ export function Services({
|
|
|
46
46
|
|
|
47
47
|
if (variant === 'list') {
|
|
48
48
|
return (
|
|
49
|
-
<section id={id} className={`py-16 ${className}`}>
|
|
49
|
+
<section id={id} className={`py-16 scroll-mt-20 ${className}`}>
|
|
50
50
|
<div className="container mx-auto px-4">
|
|
51
51
|
<SectionHeader title={title} subtitle={subtitle} />
|
|
52
52
|
<div className="max-w-3xl mx-auto divide-y divide-gray-200">
|
|
@@ -66,7 +66,7 @@ export function Services({
|
|
|
66
66
|
|
|
67
67
|
if (variant === 'minimal') {
|
|
68
68
|
return (
|
|
69
|
-
<section id={id} className={`py-16 ${className}`}>
|
|
69
|
+
<section id={id} className={`py-16 scroll-mt-20 ${className}`}>
|
|
70
70
|
<div className="container mx-auto px-4">
|
|
71
71
|
<SectionHeader title={title} subtitle={subtitle} />
|
|
72
72
|
<div className={`grid gap-6 ${gridCols[columns]}`}>
|
|
@@ -85,7 +85,7 @@ export function Services({
|
|
|
85
85
|
|
|
86
86
|
// Default: cards variant
|
|
87
87
|
return (
|
|
88
|
-
<section id={id} className={`py-16 bg-gray-50 ${className}`}>
|
|
88
|
+
<section id={id} className={`py-16 scroll-mt-20 bg-gray-50 ${className}`}>
|
|
89
89
|
<div className="container mx-auto px-4">
|
|
90
90
|
<SectionHeader title={title} subtitle={subtitle} />
|
|
91
91
|
<div className={`grid gap-6 ${gridCols[columns]}`}>
|
|
@@ -43,7 +43,7 @@ export function Testimonials({
|
|
|
43
43
|
};
|
|
44
44
|
|
|
45
45
|
return (
|
|
46
|
-
<section id={id} className={`py-16 ${className}`}>
|
|
46
|
+
<section id={id} className={`py-16 scroll-mt-20 ${className}`}>
|
|
47
47
|
<div className="container mx-auto px-4">
|
|
48
48
|
{/* Header */}
|
|
49
49
|
<div className="text-center mb-12">
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Badges Section Component
|
|
3
|
+
*
|
|
4
|
+
* Displays certifications, awards, affiliations, and trust indicators.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
export interface TrustBadge {
|
|
10
|
+
id?: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
imageUrl?: string;
|
|
14
|
+
icon?: string;
|
|
15
|
+
year?: string;
|
|
16
|
+
link?: string;
|
|
17
|
+
featured?: boolean;
|
|
18
|
+
sortOrder?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TrustBadgesProps {
|
|
22
|
+
badges: TrustBadge[];
|
|
23
|
+
title?: string;
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
variant?: 'grid' | 'inline' | 'cards';
|
|
26
|
+
columns?: 3 | 4 | 5 | 6;
|
|
27
|
+
showDescriptions?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
id?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Trust badges section component.
|
|
34
|
+
*/
|
|
35
|
+
export function TrustBadges({
|
|
36
|
+
badges,
|
|
37
|
+
title = 'Certifications & Affiliations',
|
|
38
|
+
subtitle,
|
|
39
|
+
variant = 'grid',
|
|
40
|
+
columns = 4,
|
|
41
|
+
showDescriptions = false,
|
|
42
|
+
className = '',
|
|
43
|
+
id = 'certifications',
|
|
44
|
+
}: TrustBadgesProps) {
|
|
45
|
+
const sortedBadges = [...badges].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
|
46
|
+
|
|
47
|
+
const gridCols = {
|
|
48
|
+
3: 'grid-cols-2 sm:grid-cols-3',
|
|
49
|
+
4: 'grid-cols-2 sm:grid-cols-4',
|
|
50
|
+
5: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-5',
|
|
51
|
+
6: 'grid-cols-3 sm:grid-cols-6',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (variant === 'inline') {
|
|
55
|
+
return (
|
|
56
|
+
<section id={id} className={`py-12 scroll-mt-20 bg-gray-50 ${className}`}>
|
|
57
|
+
<div className="container mx-auto px-4">
|
|
58
|
+
{title && (
|
|
59
|
+
<p className="text-center text-sm text-gray-500 mb-6">{title}</p>
|
|
60
|
+
)}
|
|
61
|
+
<div className="flex flex-wrap justify-center items-center gap-8 md:gap-12">
|
|
62
|
+
{sortedBadges.map((badge) => (
|
|
63
|
+
<InlineBadge key={badge.id || badge.name} badge={badge} />
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (variant === 'cards') {
|
|
72
|
+
return (
|
|
73
|
+
<section id={id} className={`py-16 scroll-mt-20 ${className}`}>
|
|
74
|
+
<div className="container mx-auto px-4">
|
|
75
|
+
{/* Header */}
|
|
76
|
+
<div className="text-center mb-12">
|
|
77
|
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
|
|
78
|
+
{subtitle && (
|
|
79
|
+
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
|
|
80
|
+
)}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Cards Grid */}
|
|
84
|
+
<div className={`grid gap-6 ${gridCols[columns]}`}>
|
|
85
|
+
{sortedBadges.map((badge) => (
|
|
86
|
+
<CardBadge
|
|
87
|
+
key={badge.id || badge.name}
|
|
88
|
+
badge={badge}
|
|
89
|
+
showDescription={showDescriptions}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Default: grid variant
|
|
99
|
+
return (
|
|
100
|
+
<section id={id} className={`py-16 scroll-mt-20 bg-gray-50 ${className}`}>
|
|
101
|
+
<div className="container mx-auto px-4">
|
|
102
|
+
{/* Header */}
|
|
103
|
+
<div className="text-center mb-12">
|
|
104
|
+
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
|
|
105
|
+
{subtitle && (
|
|
106
|
+
<p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Grid */}
|
|
111
|
+
<div className={`grid gap-8 ${gridCols[columns]}`}>
|
|
112
|
+
{sortedBadges.map((badge) => (
|
|
113
|
+
<GridBadge
|
|
114
|
+
key={badge.id || badge.name}
|
|
115
|
+
badge={badge}
|
|
116
|
+
showDescription={showDescriptions}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</section>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Inline badge component.
|
|
127
|
+
*/
|
|
128
|
+
function InlineBadge({ badge }: { badge: TrustBadge }) {
|
|
129
|
+
const Wrapper = badge.link ? 'a' : 'div';
|
|
130
|
+
const wrapperProps = badge.link
|
|
131
|
+
? { href: badge.link, target: '_blank', rel: 'noopener noreferrer' }
|
|
132
|
+
: {};
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<Wrapper
|
|
136
|
+
{...wrapperProps}
|
|
137
|
+
className="flex items-center opacity-70 hover:opacity-100 transition-opacity"
|
|
138
|
+
title={badge.name}
|
|
139
|
+
>
|
|
140
|
+
{badge.imageUrl ? (
|
|
141
|
+
<img
|
|
142
|
+
src={badge.imageUrl}
|
|
143
|
+
alt={badge.name}
|
|
144
|
+
className="h-10 md:h-12 w-auto object-contain grayscale hover:grayscale-0 transition-all"
|
|
145
|
+
/>
|
|
146
|
+
) : (
|
|
147
|
+
<div className="flex items-center gap-2 text-gray-600">
|
|
148
|
+
{badge.icon && <BadgeIcon icon={badge.icon} className="w-8 h-8" />}
|
|
149
|
+
<span className="text-sm font-medium">{badge.name}</span>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</Wrapper>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Grid badge component.
|
|
158
|
+
*/
|
|
159
|
+
function GridBadge({
|
|
160
|
+
badge,
|
|
161
|
+
showDescription,
|
|
162
|
+
}: {
|
|
163
|
+
badge: TrustBadge;
|
|
164
|
+
showDescription: boolean;
|
|
165
|
+
}) {
|
|
166
|
+
const Wrapper = badge.link ? 'a' : 'div';
|
|
167
|
+
const wrapperProps = badge.link
|
|
168
|
+
? { href: badge.link, target: '_blank', rel: 'noopener noreferrer' }
|
|
169
|
+
: {};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<Wrapper
|
|
173
|
+
{...wrapperProps}
|
|
174
|
+
className="flex flex-col items-center text-center group"
|
|
175
|
+
>
|
|
176
|
+
<div className="w-20 h-20 mb-3 flex items-center justify-center">
|
|
177
|
+
{badge.imageUrl ? (
|
|
178
|
+
<img
|
|
179
|
+
src={badge.imageUrl}
|
|
180
|
+
alt={badge.name}
|
|
181
|
+
className="max-w-full max-h-full object-contain group-hover:scale-110 transition-transform"
|
|
182
|
+
/>
|
|
183
|
+
) : (
|
|
184
|
+
<BadgeIcon
|
|
185
|
+
icon={badge.icon || 'certificate'}
|
|
186
|
+
className="w-16 h-16 text-primary-600 group-hover:scale-110 transition-transform"
|
|
187
|
+
/>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
<h3 className="font-semibold text-gray-900 text-sm">{badge.name}</h3>
|
|
191
|
+
{badge.year && (
|
|
192
|
+
<span className="text-xs text-gray-500">Since {badge.year}</span>
|
|
193
|
+
)}
|
|
194
|
+
{showDescription && badge.description && (
|
|
195
|
+
<p className="text-sm text-gray-600 mt-2">{badge.description}</p>
|
|
196
|
+
)}
|
|
197
|
+
</Wrapper>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Card badge component.
|
|
203
|
+
*/
|
|
204
|
+
function CardBadge({
|
|
205
|
+
badge,
|
|
206
|
+
showDescription,
|
|
207
|
+
}: {
|
|
208
|
+
badge: TrustBadge;
|
|
209
|
+
showDescription: boolean;
|
|
210
|
+
}) {
|
|
211
|
+
const Wrapper = badge.link ? 'a' : 'div';
|
|
212
|
+
const wrapperProps = badge.link
|
|
213
|
+
? { href: badge.link, target: '_blank', rel: 'noopener noreferrer' }
|
|
214
|
+
: {};
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<Wrapper
|
|
218
|
+
{...wrapperProps}
|
|
219
|
+
className="bg-white rounded-lg shadow p-6 flex flex-col items-center text-center hover:shadow-md transition-shadow"
|
|
220
|
+
>
|
|
221
|
+
<div className="w-16 h-16 mb-4 flex items-center justify-center">
|
|
222
|
+
{badge.imageUrl ? (
|
|
223
|
+
<img
|
|
224
|
+
src={badge.imageUrl}
|
|
225
|
+
alt={badge.name}
|
|
226
|
+
className="max-w-full max-h-full object-contain"
|
|
227
|
+
/>
|
|
228
|
+
) : (
|
|
229
|
+
<BadgeIcon icon={badge.icon || 'certificate'} className="w-12 h-12 text-primary-600" />
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
<h3 className="font-semibold text-gray-900">{badge.name}</h3>
|
|
233
|
+
{badge.year && (
|
|
234
|
+
<span className="text-xs text-gray-500 mt-1">Since {badge.year}</span>
|
|
235
|
+
)}
|
|
236
|
+
{showDescription && badge.description && (
|
|
237
|
+
<p className="text-sm text-gray-600 mt-2">{badge.description}</p>
|
|
238
|
+
)}
|
|
239
|
+
</Wrapper>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Badge icon component with common certification icons.
|
|
245
|
+
*/
|
|
246
|
+
function BadgeIcon({ icon, className = '' }: { icon: string; className?: string }) {
|
|
247
|
+
const icons: Record<string, JSX.Element> = {
|
|
248
|
+
certificate: (
|
|
249
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
250
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
|
251
|
+
</svg>
|
|
252
|
+
),
|
|
253
|
+
shield: (
|
|
254
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
255
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
256
|
+
</svg>
|
|
257
|
+
),
|
|
258
|
+
star: (
|
|
259
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
260
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
|
261
|
+
</svg>
|
|
262
|
+
),
|
|
263
|
+
award: (
|
|
264
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
265
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
|
266
|
+
</svg>
|
|
267
|
+
),
|
|
268
|
+
check: (
|
|
269
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
270
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
271
|
+
</svg>
|
|
272
|
+
),
|
|
273
|
+
verified: (
|
|
274
|
+
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
275
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
|
276
|
+
</svg>
|
|
277
|
+
),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return icons[icon] || icons.certificate;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export default TrustBadges;
|