Rewrite workflow copy, wire contact form to Formspree, fix contact layout
Some checks failed
Build and Deploy Website / build-and-deploy (push) Failing after 45s
Some checks failed
Build and Deploy Website / build-and-deploy (push) Failing after 45s
- Rewrote all workflow step descriptions, taglines, and deliverables with more engaging, conversational copy - Connected contact form to Formspree (xpqjzzwj) with error handling - Fixed blank space under Send Message button on desktop (items-start) - Updated test assertions to match new workflow copy
This commit is contained in:
@ -73,14 +73,18 @@ function validateForm(data: ContactFormData): FormErrors {
|
||||
return errors
|
||||
}
|
||||
|
||||
const FORMSPREE_URL = 'https://formspree.io/f/xpqjzzwj'
|
||||
|
||||
export function Contact({ onSubmit }: ContactProps) {
|
||||
const [formData, setFormData] = useState<ContactFormData>(initialFormData)
|
||||
const [errors, setErrors] = useState<FormErrors>({})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [isSuccess, setIsSuccess] = useState(false)
|
||||
const [submitError, setSubmitError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSubmitError(null)
|
||||
|
||||
const validationErrors = validateForm(formData)
|
||||
setErrors(validationErrors)
|
||||
@ -95,11 +99,19 @@ export function Contact({ onSubmit }: ContactProps) {
|
||||
if (onSubmit) {
|
||||
await onSubmit(formData)
|
||||
} else {
|
||||
// Default behavior: simulate API call
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
const res = await fetch(FORMSPREE_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify(formData),
|
||||
})
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to send message')
|
||||
}
|
||||
}
|
||||
setIsSuccess(true)
|
||||
setFormData(initialFormData)
|
||||
} catch {
|
||||
setSubmitError('Something went wrong. Please try again or email us directly.')
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
@ -120,7 +132,7 @@ export function Contact({ onSubmit }: ContactProps) {
|
||||
className="bg-slate-50 py-20 md:py-28"
|
||||
>
|
||||
<Container>
|
||||
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16">
|
||||
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16 lg:items-start">
|
||||
{/* Contact Info */}
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
@ -216,6 +228,10 @@ export function Contact({ onSubmit }: ContactProps) {
|
||||
error={errors.message}
|
||||
/>
|
||||
|
||||
{submitError && (
|
||||
<p className="text-red-600 text-sm">{submitError}</p>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
|
||||
@ -32,7 +32,7 @@ describe('WorkflowCarousel', () => {
|
||||
render(<WorkflowCarousel autoAdvance={false} />)
|
||||
|
||||
// The first step should show its description (expanded state)
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clicking a step makes it active', async () => {
|
||||
@ -44,7 +44,7 @@ describe('WorkflowCarousel', () => {
|
||||
await user.click(designStep!)
|
||||
|
||||
// Design step description should now be visible
|
||||
expect(screen.getByText(/Architecture planning/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/feels inevitable/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clicking a dot navigates to that step', async () => {
|
||||
@ -56,7 +56,7 @@ describe('WorkflowCarousel', () => {
|
||||
await user.click(dots[2])
|
||||
|
||||
// Build step description should now be visible
|
||||
expect(screen.getByText(/Agile development/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/focused sprints/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('has correct aria-current on active dot', async () => {
|
||||
@ -91,7 +91,7 @@ describe('Auto-advance', () => {
|
||||
render(<WorkflowCarousel autoAdvance={true} autoAdvanceInterval={5000} />)
|
||||
|
||||
// Initially first step is active
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
|
||||
// Advance time
|
||||
act(() => {
|
||||
@ -99,7 +99,7 @@ describe('Auto-advance', () => {
|
||||
})
|
||||
|
||||
// Second step should now be active (Design)
|
||||
expect(screen.getByText(/Architecture planning/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/feels inevitable/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('pauses on hover', () => {
|
||||
@ -116,8 +116,8 @@ describe('Auto-advance', () => {
|
||||
})
|
||||
|
||||
// Should still be on first step (paused)
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/Architecture planning/i)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/feels inevitable/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('resumes after mouse leaves', () => {
|
||||
@ -133,14 +133,14 @@ describe('Auto-advance', () => {
|
||||
fireEvent.mouseLeave(carousel)
|
||||
|
||||
// Should still be on first step
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
|
||||
// Now advance time - should move to next step
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5000)
|
||||
})
|
||||
|
||||
expect(screen.getByText(/Architecture planning/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/feels inevitable/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('loops back to first step after last', () => {
|
||||
@ -158,14 +158,14 @@ describe('Auto-advance', () => {
|
||||
})
|
||||
|
||||
// Should be on Deploy (step 4)
|
||||
expect(screen.getByText(/Production deployment/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/battle-tested/i)).toBeInTheDocument()
|
||||
|
||||
// Advance one more time - should loop back to step 1
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(5000)
|
||||
})
|
||||
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not auto-advance when autoAdvance=false', () => {
|
||||
@ -176,8 +176,8 @@ describe('Auto-advance', () => {
|
||||
})
|
||||
|
||||
// Should still be on first step
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/Architecture planning/i)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/feels inevitable/i)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -192,7 +192,7 @@ describe('Keyboard navigation', () => {
|
||||
await user.keyboard('{ArrowRight}')
|
||||
|
||||
// Second step should now be active
|
||||
expect(screen.getByText(/Architecture planning/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/feels inevitable/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('left arrow goes to previous step', async () => {
|
||||
@ -204,11 +204,11 @@ describe('Keyboard navigation', () => {
|
||||
|
||||
// Go to second step first
|
||||
await user.keyboard('{ArrowRight}')
|
||||
expect(screen.getByText(/Architecture planning/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/feels inevitable/i)).toBeInTheDocument()
|
||||
|
||||
// Go back
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('wraps around at end boundary', async () => {
|
||||
@ -220,11 +220,11 @@ describe('Keyboard navigation', () => {
|
||||
|
||||
// Go to last step
|
||||
await user.keyboard('{ArrowRight}{ArrowRight}{ArrowRight}')
|
||||
expect(screen.getByText(/Production deployment/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/battle-tested/i)).toBeInTheDocument()
|
||||
|
||||
// Arrow right should wrap to first step
|
||||
await user.keyboard('{ArrowRight}')
|
||||
expect(screen.getByText(/business requirements/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/really listen/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('wraps around at start boundary', async () => {
|
||||
@ -236,6 +236,6 @@ describe('Keyboard navigation', () => {
|
||||
|
||||
// Arrow left from first step should go to last step
|
||||
await user.keyboard('{ArrowLeft}')
|
||||
expect(screen.getByText(/Production deployment/i)).toBeInTheDocument()
|
||||
expect(screen.getByText(/battle-tested/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,36 +13,56 @@ export const workflowSteps: WorkflowStepData[] = [
|
||||
id: 1,
|
||||
icon: 'Search',
|
||||
title: 'Discovery',
|
||||
tagline: 'Understand your goals',
|
||||
description: 'We dive deep into your business requirements, user needs, and technical constraints.',
|
||||
deliverables: ['Requirements document', 'Technical roadmap', 'Project timeline'],
|
||||
tagline: 'We ask the hard questions first',
|
||||
description:
|
||||
"Before a single line of code is written, we sit down and really listen. What keeps your customers up at night? Where does your current workflow break down? We map out the landscape \u2014 your goals, your users, your constraints \u2014 so the solution we build actually solves the right problem.",
|
||||
deliverables: [
|
||||
'In-depth requirements brief',
|
||||
'Technical feasibility assessment',
|
||||
'Milestone-driven project roadmap',
|
||||
],
|
||||
image: '/images/workflow-discovery.webp',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: 'Palette',
|
||||
title: 'Design',
|
||||
tagline: 'Blueprint for success',
|
||||
description: 'Architecture planning and UI/UX design that prioritizes user experience.',
|
||||
deliverables: ['System architecture', 'UI mockups', 'Database schema'],
|
||||
tagline: 'Structure meets style',
|
||||
description:
|
||||
"Great software feels inevitable \u2014 like it couldn't have been built any other way. We design the architecture and interface together, prototyping early so you can click through real screens, not squint at wireframes. Every decision is driven by how actual humans will use the product.",
|
||||
deliverables: [
|
||||
'Interactive prototype you can share',
|
||||
'Scalable system architecture',
|
||||
'Data model and API blueprint',
|
||||
],
|
||||
image: '/images/workflow-design.webp',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: 'Code',
|
||||
title: 'Build',
|
||||
tagline: 'Code with precision',
|
||||
description: 'Agile development with regular demos and continuous integration.',
|
||||
deliverables: ['Working software', 'Test coverage', 'Documentation'],
|
||||
tagline: 'Momentum over meetings',
|
||||
description:
|
||||
"We ship working features in short, focused sprints \u2014 not slide decks. You'll see real progress every week with live demos and a running build you can test yourself. Automated tests and continuous integration mean nothing slips through the cracks as the product takes shape.",
|
||||
deliverables: [
|
||||
'Fully functional, tested features',
|
||||
'Comprehensive automated test suite',
|
||||
'Clean, documented codebase',
|
||||
],
|
||||
image: '/images/workflow-build.webp',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
icon: 'Rocket',
|
||||
title: 'Deploy',
|
||||
tagline: 'Ship with confidence',
|
||||
description: 'Production deployment with monitoring, CI/CD, and ongoing support.',
|
||||
deliverables: ['Production environment', 'Monitoring setup', 'Support plan'],
|
||||
tagline: 'Launch day is the easy part',
|
||||
description:
|
||||
"By the time we go live, everything has already been battle-tested. We handle infrastructure, CI/CD pipelines, monitoring, and the inevitable \"what if traffic spikes?\" questions. And we stick around after launch \u2014 because the best products keep getting better.",
|
||||
deliverables: [
|
||||
'Production-ready infrastructure',
|
||||
'Real-time monitoring and alerts',
|
||||
'Ongoing support and iteration plan',
|
||||
],
|
||||
image: '/images/workflow-deploy.webp',
|
||||
},
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user