@aws505/sheetsite 1.0.2 → 1.0.4
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 +932 -60
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +916 -60
- package/dist/components/index.mjs.map +1 -1
- package/dist/index.js +195 -62
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +15 -0
- package/src/components/layout/Header.tsx +1 -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/Hero.tsx +19 -3
- 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
- package/src/components/ui/FloatingClaimBanner.tsx +160 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animated Section Component
|
|
3
|
+
*
|
|
4
|
+
* A wrapper component that adds scroll-triggered animations to its children.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
export interface AnimatedSectionProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
animation?: 'fade-up' | 'fade-down' | 'fade-left' | 'fade-right' | 'zoom' | 'none';
|
|
14
|
+
delay?: number;
|
|
15
|
+
duration?: number;
|
|
16
|
+
threshold?: number;
|
|
17
|
+
once?: boolean;
|
|
18
|
+
className?: string;
|
|
19
|
+
as?: React.ElementType;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Animated section wrapper with scroll-triggered animations.
|
|
24
|
+
*/
|
|
25
|
+
export function AnimatedSection({
|
|
26
|
+
children,
|
|
27
|
+
animation = 'fade-up',
|
|
28
|
+
delay = 0,
|
|
29
|
+
duration = 600,
|
|
30
|
+
threshold = 0.1,
|
|
31
|
+
once = true,
|
|
32
|
+
className = '',
|
|
33
|
+
as: Component = 'div',
|
|
34
|
+
}: AnimatedSectionProps) {
|
|
35
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
36
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const observer = new IntersectionObserver(
|
|
40
|
+
([entry]) => {
|
|
41
|
+
if (entry.isIntersecting) {
|
|
42
|
+
setIsVisible(true);
|
|
43
|
+
if (once && ref.current) {
|
|
44
|
+
observer.unobserve(ref.current);
|
|
45
|
+
}
|
|
46
|
+
} else if (!once) {
|
|
47
|
+
setIsVisible(false);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{ threshold }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (ref.current) {
|
|
54
|
+
observer.observe(ref.current);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
if (ref.current) {
|
|
59
|
+
observer.unobserve(ref.current);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}, [threshold, once]);
|
|
63
|
+
|
|
64
|
+
const getAnimationStyles = (): React.CSSProperties => {
|
|
65
|
+
const baseStyles: React.CSSProperties = {
|
|
66
|
+
transition: `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`,
|
|
67
|
+
transitionDelay: `${delay}ms`,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (!isVisible) {
|
|
71
|
+
switch (animation) {
|
|
72
|
+
case 'fade-up':
|
|
73
|
+
return { ...baseStyles, opacity: 0, transform: 'translateY(30px)' };
|
|
74
|
+
case 'fade-down':
|
|
75
|
+
return { ...baseStyles, opacity: 0, transform: 'translateY(-30px)' };
|
|
76
|
+
case 'fade-left':
|
|
77
|
+
return { ...baseStyles, opacity: 0, transform: 'translateX(30px)' };
|
|
78
|
+
case 'fade-right':
|
|
79
|
+
return { ...baseStyles, opacity: 0, transform: 'translateX(-30px)' };
|
|
80
|
+
case 'zoom':
|
|
81
|
+
return { ...baseStyles, opacity: 0, transform: 'scale(0.95)' };
|
|
82
|
+
case 'none':
|
|
83
|
+
return {};
|
|
84
|
+
default:
|
|
85
|
+
return { ...baseStyles, opacity: 0, transform: 'translateY(30px)' };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { ...baseStyles, opacity: 1, transform: 'translateY(0) translateX(0) scale(1)' };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (animation === 'none') {
|
|
93
|
+
return <Component className={className}>{children}</Component>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<Component ref={ref} style={getAnimationStyles()} className={className}>
|
|
98
|
+
{children}
|
|
99
|
+
</Component>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Stagger animation container - animates children with increasing delays.
|
|
105
|
+
*/
|
|
106
|
+
export interface StaggerContainerProps {
|
|
107
|
+
children: React.ReactNode;
|
|
108
|
+
staggerDelay?: number;
|
|
109
|
+
animation?: AnimatedSectionProps['animation'];
|
|
110
|
+
duration?: number;
|
|
111
|
+
className?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function StaggerContainer({
|
|
115
|
+
children,
|
|
116
|
+
staggerDelay = 100,
|
|
117
|
+
animation = 'fade-up',
|
|
118
|
+
duration = 600,
|
|
119
|
+
className = '',
|
|
120
|
+
}: StaggerContainerProps) {
|
|
121
|
+
return (
|
|
122
|
+
<div className={className}>
|
|
123
|
+
{React.Children.map(children, (child, index) => (
|
|
124
|
+
<AnimatedSection
|
|
125
|
+
animation={animation}
|
|
126
|
+
delay={index * staggerDelay}
|
|
127
|
+
duration={duration}
|
|
128
|
+
>
|
|
129
|
+
{child}
|
|
130
|
+
</AnimatedSection>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default AnimatedSection;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Floating Claim Banner Component
|
|
3
|
+
*
|
|
4
|
+
* A floating notification banner for business owners to claim their website.
|
|
5
|
+
* Used for sales outreach purposes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { useState, useEffect } from 'react';
|
|
11
|
+
|
|
12
|
+
export interface FloatingClaimBannerProps {
|
|
13
|
+
/** The email address to send claim requests to */
|
|
14
|
+
contactEmail?: string;
|
|
15
|
+
/** The site URL to include in the email subject (defaults to window.location.hostname) */
|
|
16
|
+
siteUrl?: string;
|
|
17
|
+
/** Business name for the email */
|
|
18
|
+
businessName?: string;
|
|
19
|
+
/** Position of the banner */
|
|
20
|
+
position?: 'bottom-right' | 'bottom-left' | 'bottom-center';
|
|
21
|
+
/** Delay before showing the banner (ms) */
|
|
22
|
+
showDelay?: number;
|
|
23
|
+
/** Whether the banner can be dismissed */
|
|
24
|
+
dismissible?: boolean;
|
|
25
|
+
/** Custom message text */
|
|
26
|
+
message?: string;
|
|
27
|
+
/** Custom button text */
|
|
28
|
+
buttonText?: string;
|
|
29
|
+
/** Custom class name */
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Floating banner for business owners to claim their site.
|
|
35
|
+
*/
|
|
36
|
+
export function FloatingClaimBanner({
|
|
37
|
+
contactEmail = 'andrew@whotookmy.com',
|
|
38
|
+
siteUrl,
|
|
39
|
+
businessName,
|
|
40
|
+
position = 'bottom-right',
|
|
41
|
+
showDelay = 3000,
|
|
42
|
+
dismissible = true,
|
|
43
|
+
message = 'Is this your business?',
|
|
44
|
+
buttonText = 'Claim This Site',
|
|
45
|
+
className = '',
|
|
46
|
+
}: FloatingClaimBannerProps) {
|
|
47
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
48
|
+
const [isDismissed, setIsDismissed] = useState(false);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
// Check if already dismissed in this session
|
|
52
|
+
const dismissed = sessionStorage.getItem('claimBannerDismissed');
|
|
53
|
+
if (dismissed) {
|
|
54
|
+
setIsDismissed(true);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Show banner after delay
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
setIsVisible(true);
|
|
61
|
+
}, showDelay);
|
|
62
|
+
|
|
63
|
+
return () => clearTimeout(timer);
|
|
64
|
+
}, [showDelay]);
|
|
65
|
+
|
|
66
|
+
const handleDismiss = () => {
|
|
67
|
+
setIsVisible(false);
|
|
68
|
+
setIsDismissed(true);
|
|
69
|
+
sessionStorage.setItem('claimBannerDismissed', 'true');
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleClaim = () => {
|
|
73
|
+
const url = siteUrl || (typeof window !== 'undefined' ? window.location.hostname : 'unknown');
|
|
74
|
+
const subject = encodeURIComponent(`Claim my site - ${url}`);
|
|
75
|
+
const body = encodeURIComponent(
|
|
76
|
+
`Hi,\n\nI am the owner of ${businessName || 'this business'} and I would like to claim my website at ${url}.\n\nPlease contact me to discuss.\n\nThank you!`
|
|
77
|
+
);
|
|
78
|
+
window.location.href = `mailto:${contactEmail}?subject=${subject}&body=${body}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (isDismissed || !isVisible) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const positionClasses = {
|
|
86
|
+
'bottom-right': 'bottom-4 right-4',
|
|
87
|
+
'bottom-left': 'bottom-4 left-4',
|
|
88
|
+
'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className={`
|
|
94
|
+
fixed z-50 ${positionClasses[position]}
|
|
95
|
+
animate-slide-up
|
|
96
|
+
${className}
|
|
97
|
+
`}
|
|
98
|
+
style={{
|
|
99
|
+
animation: 'slideUp 0.5s ease-out',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<div className="bg-white rounded-lg shadow-2xl border border-gray-200 p-4 max-w-sm">
|
|
103
|
+
<div className="flex items-start gap-3">
|
|
104
|
+
{/* Icon */}
|
|
105
|
+
<div className="flex-shrink-0 w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
|
|
106
|
+
<svg className="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
107
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
108
|
+
</svg>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{/* Content */}
|
|
112
|
+
<div className="flex-1 min-w-0">
|
|
113
|
+
<p className="text-sm font-medium text-gray-900">{message}</p>
|
|
114
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
115
|
+
We built this site for you. Claim it today!
|
|
116
|
+
</p>
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleClaim}
|
|
119
|
+
className="mt-3 w-full inline-flex items-center justify-center px-4 py-2 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition-colors"
|
|
120
|
+
>
|
|
121
|
+
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
122
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
123
|
+
</svg>
|
|
124
|
+
{buttonText}
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Dismiss button */}
|
|
129
|
+
{dismissible && (
|
|
130
|
+
<button
|
|
131
|
+
onClick={handleDismiss}
|
|
132
|
+
className="flex-shrink-0 text-gray-400 hover:text-gray-600 transition-colors"
|
|
133
|
+
aria-label="Dismiss"
|
|
134
|
+
>
|
|
135
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
136
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
137
|
+
</svg>
|
|
138
|
+
</button>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Animation keyframes */}
|
|
144
|
+
<style jsx>{`
|
|
145
|
+
@keyframes slideUp {
|
|
146
|
+
from {
|
|
147
|
+
opacity: 0;
|
|
148
|
+
transform: translateY(20px);
|
|
149
|
+
}
|
|
150
|
+
to {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
transform: translateY(0);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
`}</style>
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export default FloatingClaimBanner;
|