@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,184 @@
1
+ /**
2
+ * Testimonials Section Component
3
+ *
4
+ * Displays customer reviews and testimonials.
5
+ */
6
+
7
+ import React from 'react';
8
+ import type { Testimonial } from '../../data/types';
9
+ import { StarIcon } from '../ui/Icons';
10
+
11
+ export interface TestimonialsProps {
12
+ testimonials: Testimonial[];
13
+ title?: string;
14
+ subtitle?: string;
15
+ columns?: 1 | 2 | 3;
16
+ showRatings?: boolean;
17
+ variant?: 'cards' | 'quotes' | 'minimal';
18
+ limit?: number;
19
+ className?: string;
20
+ }
21
+
22
+ /**
23
+ * Testimonials section component.
24
+ */
25
+ export function Testimonials({
26
+ testimonials,
27
+ title = 'What Our Customers Say',
28
+ subtitle,
29
+ columns = 3,
30
+ showRatings = true,
31
+ variant = 'cards',
32
+ limit,
33
+ className = '',
34
+ }: TestimonialsProps) {
35
+ const displayedTestimonials = limit ? testimonials.slice(0, limit) : testimonials;
36
+
37
+ const gridCols = {
38
+ 1: 'max-w-2xl mx-auto',
39
+ 2: 'md:grid-cols-2 max-w-4xl mx-auto',
40
+ 3: 'md:grid-cols-2 lg:grid-cols-3',
41
+ };
42
+
43
+ return (
44
+ <section className={`py-16 ${className}`}>
45
+ <div className="container mx-auto px-4">
46
+ {/* Header */}
47
+ <div className="text-center mb-12">
48
+ <h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">{title}</h2>
49
+ {subtitle && (
50
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>
51
+ )}
52
+ </div>
53
+
54
+ {/* Testimonials Grid */}
55
+ <div className={`grid gap-6 ${gridCols[columns]}`}>
56
+ {displayedTestimonials.map((testimonial) => (
57
+ <TestimonialCard
58
+ key={testimonial.id || testimonial.name}
59
+ testimonial={testimonial}
60
+ showRating={showRatings}
61
+ variant={variant}
62
+ />
63
+ ))}
64
+ </div>
65
+ </div>
66
+ </section>
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Individual testimonial card.
72
+ */
73
+ function TestimonialCard({
74
+ testimonial,
75
+ showRating,
76
+ variant,
77
+ }: {
78
+ testimonial: Testimonial;
79
+ showRating: boolean;
80
+ variant: TestimonialsProps['variant'];
81
+ }) {
82
+ if (variant === 'quotes') {
83
+ return (
84
+ <div className="text-center">
85
+ <QuoteIcon className="w-10 h-10 text-primary-200 mx-auto mb-4" />
86
+ <blockquote className="text-lg text-gray-700 italic mb-4">
87
+ "{testimonial.quote}"
88
+ </blockquote>
89
+ {showRating && testimonial.rating && (
90
+ <StarRating rating={testimonial.rating} className="justify-center mb-2" />
91
+ )}
92
+ <div className="font-semibold text-gray-900">{testimonial.name}</div>
93
+ {testimonial.context && (
94
+ <div className="text-sm text-gray-500">{testimonial.context}</div>
95
+ )}
96
+ </div>
97
+ );
98
+ }
99
+
100
+ if (variant === 'minimal') {
101
+ return (
102
+ <div className="border-l-4 border-primary-500 pl-4">
103
+ <blockquote className="text-gray-700 mb-2">
104
+ "{testimonial.quote}"
105
+ </blockquote>
106
+ <div className="flex items-center gap-2">
107
+ <span className="font-semibold text-gray-900">{testimonial.name}</span>
108
+ {showRating && testimonial.rating && (
109
+ <StarRating rating={testimonial.rating} size={14} />
110
+ )}
111
+ </div>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ // Default: cards variant
117
+ return (
118
+ <div className="bg-white rounded-lg shadow p-6">
119
+ {showRating && testimonial.rating && (
120
+ <StarRating rating={testimonial.rating} className="mb-3" />
121
+ )}
122
+ <blockquote className="text-gray-700 mb-4">
123
+ "{testimonial.quote}"
124
+ </blockquote>
125
+ <div className="flex items-center">
126
+ {testimonial.imageUrl && (
127
+ <img
128
+ src={testimonial.imageUrl}
129
+ alt={testimonial.name}
130
+ className="w-10 h-10 rounded-full mr-3 object-cover"
131
+ />
132
+ )}
133
+ <div>
134
+ <div className="font-semibold text-gray-900">{testimonial.name}</div>
135
+ {testimonial.context && (
136
+ <div className="text-sm text-gray-500">{testimonial.context}</div>
137
+ )}
138
+ {testimonial.source && (
139
+ <div className="text-xs text-gray-400">via {testimonial.source}</div>
140
+ )}
141
+ </div>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Star rating component.
149
+ */
150
+ function StarRating({
151
+ rating,
152
+ size = 18,
153
+ className = '',
154
+ }: {
155
+ rating: number;
156
+ size?: number;
157
+ className?: string;
158
+ }) {
159
+ return (
160
+ <div className={`flex ${className}`} aria-label={`${rating} out of 5 stars`}>
161
+ {[1, 2, 3, 4, 5].map((star) => (
162
+ <StarIcon
163
+ key={star}
164
+ size={size}
165
+ filled={star <= rating}
166
+ className={star <= rating ? 'text-yellow-400' : 'text-gray-300'}
167
+ />
168
+ ))}
169
+ </div>
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Quote icon.
175
+ */
176
+ function QuoteIcon({ className }: { className?: string }) {
177
+ return (
178
+ <svg className={className} fill="currentColor" viewBox="0 0 24 24">
179
+ <path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
180
+ </svg>
181
+ );
182
+ }
183
+
184
+ export default Testimonials;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Button Component
3
+ *
4
+ * A versatile button component with multiple variants and sizes.
5
+ */
6
+
7
+ import React from 'react';
8
+
9
+ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
10
+ variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link';
11
+ size?: 'sm' | 'md' | 'lg';
12
+ fullWidth?: boolean;
13
+ loading?: boolean;
14
+ leftIcon?: React.ReactNode;
15
+ rightIcon?: React.ReactNode;
16
+ }
17
+
18
+ /**
19
+ * Get CSS classes for button variants and sizes.
20
+ */
21
+ function getButtonClasses(props: Pick<ButtonProps, 'variant' | 'size' | 'fullWidth' | 'disabled'>): string {
22
+ const { variant = 'primary', size = 'md', fullWidth, disabled } = props;
23
+
24
+ const baseClasses = [
25
+ 'inline-flex items-center justify-center',
26
+ 'font-medium',
27
+ 'transition-colors duration-200',
28
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2',
29
+ disabled ? 'opacity-50 cursor-not-allowed' : '',
30
+ fullWidth ? 'w-full' : '',
31
+ ];
32
+
33
+ // Variant classes
34
+ const variantClasses: Record<NonNullable<ButtonProps['variant']>, string> = {
35
+ primary: 'bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500',
36
+ secondary: 'bg-primary-100 text-primary-700 hover:bg-primary-200 focus:ring-primary-500',
37
+ outline: 'border-2 border-primary-600 text-primary-600 hover:bg-primary-50 focus:ring-primary-500',
38
+ ghost: 'text-primary-600 hover:bg-primary-100 focus:ring-primary-500',
39
+ link: 'text-primary-600 hover:underline focus:ring-primary-500 p-0',
40
+ };
41
+
42
+ // Size classes
43
+ const sizeClasses: Record<NonNullable<ButtonProps['size']>, string> = {
44
+ sm: 'text-sm px-3 py-1.5 rounded',
45
+ md: 'text-base px-4 py-2 rounded-md',
46
+ lg: 'text-lg px-6 py-3 rounded-lg',
47
+ };
48
+
49
+ return [
50
+ ...baseClasses,
51
+ variantClasses[variant],
52
+ variant !== 'link' ? sizeClasses[size] : '',
53
+ ]
54
+ .filter(Boolean)
55
+ .join(' ');
56
+ }
57
+
58
+ /**
59
+ * Button component with multiple variants and sizes.
60
+ */
61
+ export function Button({
62
+ variant = 'primary',
63
+ size = 'md',
64
+ fullWidth = false,
65
+ loading = false,
66
+ leftIcon,
67
+ rightIcon,
68
+ children,
69
+ disabled,
70
+ className = '',
71
+ ...props
72
+ }: ButtonProps) {
73
+ const classes = getButtonClasses({ variant, size, fullWidth, disabled: disabled || loading });
74
+
75
+ return (
76
+ <button
77
+ className={`${classes} ${className}`}
78
+ disabled={disabled || loading}
79
+ {...props}
80
+ >
81
+ {loading ? (
82
+ <LoadingSpinner size={size} />
83
+ ) : (
84
+ <>
85
+ {leftIcon && <span className="mr-2">{leftIcon}</span>}
86
+ {children}
87
+ {rightIcon && <span className="ml-2">{rightIcon}</span>}
88
+ </>
89
+ )}
90
+ </button>
91
+ );
92
+ }
93
+
94
+ /**
95
+ * Loading spinner for button loading state.
96
+ */
97
+ function LoadingSpinner({ size }: { size: ButtonProps['size'] }) {
98
+ const sizeClasses = {
99
+ sm: 'w-4 h-4',
100
+ md: 'w-5 h-5',
101
+ lg: 'w-6 h-6',
102
+ };
103
+
104
+ return (
105
+ <svg
106
+ className={`animate-spin ${sizeClasses[size || 'md']}`}
107
+ xmlns="http://www.w3.org/2000/svg"
108
+ fill="none"
109
+ viewBox="0 0 24 24"
110
+ >
111
+ <circle
112
+ className="opacity-25"
113
+ cx="12"
114
+ cy="12"
115
+ r="10"
116
+ stroke="currentColor"
117
+ strokeWidth="4"
118
+ />
119
+ <path
120
+ className="opacity-75"
121
+ fill="currentColor"
122
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
123
+ />
124
+ </svg>
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Link styled as a button.
130
+ */
131
+ export interface ButtonLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
132
+ variant?: ButtonProps['variant'];
133
+ size?: ButtonProps['size'];
134
+ fullWidth?: boolean;
135
+ leftIcon?: React.ReactNode;
136
+ rightIcon?: React.ReactNode;
137
+ }
138
+
139
+ export function ButtonLink({
140
+ variant = 'primary',
141
+ size = 'md',
142
+ fullWidth = false,
143
+ leftIcon,
144
+ rightIcon,
145
+ children,
146
+ className = '',
147
+ ...props
148
+ }: ButtonLinkProps) {
149
+ const classes = getButtonClasses({ variant, size, fullWidth, disabled: false });
150
+
151
+ return (
152
+ <a className={`${classes} ${className}`} {...props}>
153
+ {leftIcon && <span className="mr-2">{leftIcon}</span>}
154
+ {children}
155
+ {rightIcon && <span className="ml-2">{rightIcon}</span>}
156
+ </a>
157
+ );
158
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Card Component
3
+ *
4
+ * A flexible card component for displaying content in a contained box.
5
+ */
6
+
7
+ import React from 'react';
8
+
9
+ export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
10
+ variant?: 'default' | 'elevated' | 'outlined' | 'filled';
11
+ padding?: 'none' | 'sm' | 'md' | 'lg';
12
+ hover?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Get CSS classes for card variants.
17
+ */
18
+ function getCardClasses(props: Pick<CardProps, 'variant' | 'padding' | 'hover'>): string {
19
+ const { variant = 'default', padding = 'md', hover = false } = props;
20
+
21
+ const baseClasses = ['rounded-lg', 'overflow-hidden'];
22
+
23
+ // Variant classes
24
+ const variantClasses: Record<NonNullable<CardProps['variant']>, string> = {
25
+ default: 'bg-white shadow',
26
+ elevated: 'bg-white shadow-lg',
27
+ outlined: 'bg-white border border-gray-200',
28
+ filled: 'bg-gray-50',
29
+ };
30
+
31
+ // Padding classes
32
+ const paddingClasses: Record<NonNullable<CardProps['padding']>, string> = {
33
+ none: '',
34
+ sm: 'p-3',
35
+ md: 'p-5',
36
+ lg: 'p-8',
37
+ };
38
+
39
+ // Hover effect
40
+ const hoverClasses = hover
41
+ ? 'transition-all duration-200 hover:shadow-lg hover:-translate-y-1'
42
+ : '';
43
+
44
+ return [
45
+ ...baseClasses,
46
+ variantClasses[variant],
47
+ paddingClasses[padding],
48
+ hoverClasses,
49
+ ]
50
+ .filter(Boolean)
51
+ .join(' ');
52
+ }
53
+
54
+ /**
55
+ * Card component for containing content.
56
+ */
57
+ export function Card({
58
+ variant = 'default',
59
+ padding = 'md',
60
+ hover = false,
61
+ children,
62
+ className = '',
63
+ ...props
64
+ }: CardProps) {
65
+ const classes = getCardClasses({ variant, padding, hover });
66
+
67
+ return (
68
+ <div className={`${classes} ${className}`} {...props}>
69
+ {children}
70
+ </div>
71
+ );
72
+ }
73
+
74
+ /**
75
+ * Card header section.
76
+ */
77
+ export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
78
+ title?: string;
79
+ subtitle?: string;
80
+ action?: React.ReactNode;
81
+ }
82
+
83
+ export function CardHeader({
84
+ title,
85
+ subtitle,
86
+ action,
87
+ children,
88
+ className = '',
89
+ ...props
90
+ }: CardHeaderProps) {
91
+ return (
92
+ <div className={`flex items-start justify-between ${className}`} {...props}>
93
+ <div>
94
+ {title && <h3 className="text-lg font-semibold text-gray-900">{title}</h3>}
95
+ {subtitle && <p className="mt-1 text-sm text-gray-500">{subtitle}</p>}
96
+ {children}
97
+ </div>
98
+ {action && <div className="flex-shrink-0 ml-4">{action}</div>}
99
+ </div>
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Card body section.
105
+ */
106
+ export function CardBody({
107
+ children,
108
+ className = '',
109
+ ...props
110
+ }: React.HTMLAttributes<HTMLDivElement>) {
111
+ return (
112
+ <div className={`mt-4 ${className}`} {...props}>
113
+ {children}
114
+ </div>
115
+ );
116
+ }
117
+
118
+ /**
119
+ * Card footer section.
120
+ */
121
+ export function CardFooter({
122
+ children,
123
+ className = '',
124
+ ...props
125
+ }: React.HTMLAttributes<HTMLDivElement>) {
126
+ return (
127
+ <div className={`mt-4 pt-4 border-t border-gray-100 ${className}`} {...props}>
128
+ {children}
129
+ </div>
130
+ );
131
+ }
132
+
133
+ /**
134
+ * Card image section.
135
+ */
136
+ export interface CardImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
137
+ aspectRatio?: 'square' | 'video' | 'wide' | 'auto';
138
+ }
139
+
140
+ export function CardImage({
141
+ aspectRatio = 'video',
142
+ className = '',
143
+ alt = '',
144
+ ...props
145
+ }: CardImageProps) {
146
+ const aspectClasses: Record<NonNullable<CardImageProps['aspectRatio']>, string> = {
147
+ square: 'aspect-square',
148
+ video: 'aspect-video',
149
+ wide: 'aspect-[2/1]',
150
+ auto: '',
151
+ };
152
+
153
+ return (
154
+ <div className={`-m-5 mb-4 ${aspectClasses[aspectRatio]} overflow-hidden`}>
155
+ <img
156
+ className={`w-full h-full object-cover ${className}`}
157
+ alt={alt}
158
+ {...props}
159
+ />
160
+ </div>
161
+ );
162
+ }