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

- 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:
2026-02-06 10:44:56 -07:00
parent 5786af6359
commit e9b5dccbe2
3 changed files with 70 additions and 34 deletions

View File

@ -73,14 +73,18 @@ function validateForm(data: ContactFormData): FormErrors {
return errors return errors
} }
const FORMSPREE_URL = 'https://formspree.io/f/xpqjzzwj'
export function Contact({ onSubmit }: ContactProps) { export function Contact({ onSubmit }: ContactProps) {
const [formData, setFormData] = useState<ContactFormData>(initialFormData) const [formData, setFormData] = useState<ContactFormData>(initialFormData)
const [errors, setErrors] = useState<FormErrors>({}) const [errors, setErrors] = useState<FormErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false) const [isSuccess, setIsSuccess] = useState(false)
const [submitError, setSubmitError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
setSubmitError(null)
const validationErrors = validateForm(formData) const validationErrors = validateForm(formData)
setErrors(validationErrors) setErrors(validationErrors)
@ -95,11 +99,19 @@ export function Contact({ onSubmit }: ContactProps) {
if (onSubmit) { if (onSubmit) {
await onSubmit(formData) await onSubmit(formData)
} else { } else {
// Default behavior: simulate API call const res = await fetch(FORMSPREE_URL, {
await new Promise((resolve) => setTimeout(resolve, 1000)) 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) setIsSuccess(true)
setFormData(initialFormData) setFormData(initialFormData)
} catch {
setSubmitError('Something went wrong. Please try again or email us directly.')
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false)
} }
@ -120,7 +132,7 @@ export function Contact({ onSubmit }: ContactProps) {
className="bg-slate-50 py-20 md:py-28" className="bg-slate-50 py-20 md:py-28"
> >
<Container> <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 */} {/* Contact Info */}
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
@ -216,6 +228,10 @@ export function Contact({ onSubmit }: ContactProps) {
error={errors.message} error={errors.message}
/> />
{submitError && (
<p className="text-red-600 text-sm">{submitError}</p>
)}
<Button <Button
variant="primary" variant="primary"
type="submit" type="submit"

View File

@ -32,7 +32,7 @@ describe('WorkflowCarousel', () => {
render(<WorkflowCarousel autoAdvance={false} />) render(<WorkflowCarousel autoAdvance={false} />)
// The first step should show its description (expanded state) // 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 () => { it('clicking a step makes it active', async () => {
@ -44,7 +44,7 @@ describe('WorkflowCarousel', () => {
await user.click(designStep!) await user.click(designStep!)
// Design step description should now be visible // 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 () => { it('clicking a dot navigates to that step', async () => {
@ -56,7 +56,7 @@ describe('WorkflowCarousel', () => {
await user.click(dots[2]) await user.click(dots[2])
// Build step description should now be visible // 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 () => { it('has correct aria-current on active dot', async () => {
@ -91,7 +91,7 @@ describe('Auto-advance', () => {
render(<WorkflowCarousel autoAdvance={true} autoAdvanceInterval={5000} />) render(<WorkflowCarousel autoAdvance={true} autoAdvanceInterval={5000} />)
// Initially first step is active // Initially first step is active
expect(screen.getByText(/business requirements/i)).toBeInTheDocument() expect(screen.getByText(/really listen/i)).toBeInTheDocument()
// Advance time // Advance time
act(() => { act(() => {
@ -99,7 +99,7 @@ describe('Auto-advance', () => {
}) })
// Second step should now be active (Design) // 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', () => { it('pauses on hover', () => {
@ -116,8 +116,8 @@ describe('Auto-advance', () => {
}) })
// Should still be on first step (paused) // Should still be on first step (paused)
expect(screen.getByText(/business requirements/i)).toBeInTheDocument() expect(screen.getByText(/really listen/i)).toBeInTheDocument()
expect(screen.queryByText(/Architecture planning/i)).not.toBeInTheDocument() expect(screen.queryByText(/feels inevitable/i)).not.toBeInTheDocument()
}) })
it('resumes after mouse leaves', () => { it('resumes after mouse leaves', () => {
@ -133,14 +133,14 @@ describe('Auto-advance', () => {
fireEvent.mouseLeave(carousel) fireEvent.mouseLeave(carousel)
// Should still be on first step // 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 // Now advance time - should move to next step
act(() => { act(() => {
vi.advanceTimersByTime(5000) 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', () => { it('loops back to first step after last', () => {
@ -158,14 +158,14 @@ describe('Auto-advance', () => {
}) })
// Should be on Deploy (step 4) // 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 // Advance one more time - should loop back to step 1
act(() => { act(() => {
vi.advanceTimersByTime(5000) 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', () => { it('does not auto-advance when autoAdvance=false', () => {
@ -176,8 +176,8 @@ describe('Auto-advance', () => {
}) })
// Should still be on first step // Should still be on first step
expect(screen.getByText(/business requirements/i)).toBeInTheDocument() expect(screen.getByText(/really listen/i)).toBeInTheDocument()
expect(screen.queryByText(/Architecture planning/i)).not.toBeInTheDocument() expect(screen.queryByText(/feels inevitable/i)).not.toBeInTheDocument()
}) })
}) })
@ -192,7 +192,7 @@ describe('Keyboard navigation', () => {
await user.keyboard('{ArrowRight}') await user.keyboard('{ArrowRight}')
// Second step should now be active // 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 () => { it('left arrow goes to previous step', async () => {
@ -204,11 +204,11 @@ describe('Keyboard navigation', () => {
// Go to second step first // Go to second step first
await user.keyboard('{ArrowRight}') await user.keyboard('{ArrowRight}')
expect(screen.getByText(/Architecture planning/i)).toBeInTheDocument() expect(screen.getByText(/feels inevitable/i)).toBeInTheDocument()
// Go back // Go back
await user.keyboard('{ArrowLeft}') 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 () => { it('wraps around at end boundary', async () => {
@ -220,11 +220,11 @@ describe('Keyboard navigation', () => {
// Go to last step // Go to last step
await user.keyboard('{ArrowRight}{ArrowRight}{ArrowRight}') 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 // Arrow right should wrap to first step
await user.keyboard('{ArrowRight}') 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 () => { it('wraps around at start boundary', async () => {
@ -236,6 +236,6 @@ describe('Keyboard navigation', () => {
// Arrow left from first step should go to last step // Arrow left from first step should go to last step
await user.keyboard('{ArrowLeft}') await user.keyboard('{ArrowLeft}')
expect(screen.getByText(/Production deployment/i)).toBeInTheDocument() expect(screen.getByText(/battle-tested/i)).toBeInTheDocument()
}) })
}) })

View File

@ -13,36 +13,56 @@ export const workflowSteps: WorkflowStepData[] = [
id: 1, id: 1,
icon: 'Search', icon: 'Search',
title: 'Discovery', title: 'Discovery',
tagline: 'Understand your goals', tagline: 'We ask the hard questions first',
description: 'We dive deep into your business requirements, user needs, and technical constraints.', description:
deliverables: ['Requirements document', 'Technical roadmap', 'Project timeline'], "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', image: '/images/workflow-discovery.webp',
}, },
{ {
id: 2, id: 2,
icon: 'Palette', icon: 'Palette',
title: 'Design', title: 'Design',
tagline: 'Blueprint for success', tagline: 'Structure meets style',
description: 'Architecture planning and UI/UX design that prioritizes user experience.', description:
deliverables: ['System architecture', 'UI mockups', 'Database schema'], "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', image: '/images/workflow-design.webp',
}, },
{ {
id: 3, id: 3,
icon: 'Code', icon: 'Code',
title: 'Build', title: 'Build',
tagline: 'Code with precision', tagline: 'Momentum over meetings',
description: 'Agile development with regular demos and continuous integration.', description:
deliverables: ['Working software', 'Test coverage', 'Documentation'], "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', image: '/images/workflow-build.webp',
}, },
{ {
id: 4, id: 4,
icon: 'Rocket', icon: 'Rocket',
title: 'Deploy', title: 'Deploy',
tagline: 'Ship with confidence', tagline: 'Launch day is the easy part',
description: 'Production deployment with monitoring, CI/CD, and ongoing support.', description:
deliverables: ['Production environment', 'Monitoring setup', 'Support plan'], "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', image: '/images/workflow-deploy.webp',
}, },
] ]