Complete Phase 4: Polish, Docker, CI/CD, and deployment readiness

- Fix responsive issues: Card/detail panel use p-4 md:p-8, larger
  progress dots, tighter mobile gaps in Contact section
- Fix lint errors: remove unused vars in BentoCard, AnimatedSection;
  fix explicit-any in ExpandableServiceCard test
- Fix zod-validation-error v4 dependency for eslint-plugin-react-hooks
- Add type-check script to package.json
- Create multi-stage Dockerfile (node:20-alpine build, nginx:alpine serve)
- Create .dockerignore to exclude dev files from build context
- Create nginx.conf with SPA routing, gzip, caching, security headers
- Update docker-compose.yml: build context replaces volume mount,
  preserve Traefik labels, add healthcheck
- Rewrite CI/CD pipeline: Node.js setup, npm ci, type-check, lint,
  test, build, rsync + docker compose build on server
- Add AI-generated images for all placeholder locations
- Archive original index.html as index.html.original

All checks pass: tsc --noEmit, eslint, 222/222 tests, vite build

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 00:56:07 -07:00
parent 2a59beeac8
commit eccaf8a60d
26 changed files with 229 additions and 824 deletions

View File

@ -36,8 +36,6 @@ export function BentoCard({
'data-testid': testId,
}: BentoCardProps) {
const Icon = iconMap[service.icon] || Globe
const showDetails = size === 'lg' || size === 'md'
const showTechnologies = size === 'lg'
return (
<div

View File

@ -149,7 +149,7 @@ describe('ExpandableServiceCard', () => {
it('works without technologies prop', async () => {
const user = userEvent.setup()
const propsWithoutTech = { ...defaultProps }
delete (propsWithoutTech as any).technologies
delete (propsWithoutTech as Record<string, unknown>).technologies
render(<ExpandableServiceCard {...propsWithoutTech} />)

View File

@ -120,7 +120,7 @@ export function Contact({ onSubmit }: ContactProps) {
className="bg-slate-50 py-20 md:py-28"
>
<Container>
<div className="grid gap-12 lg:grid-cols-2 lg:gap-16">
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16">
{/* Contact Info */}
<div className="space-y-8">
<div>

View File

@ -86,7 +86,7 @@ export function WorkflowCarousel({
animate={{ opacity: 1 }}
exit={{ opacity: 1 }}
transition={{ duration: 1.0, ease: 'easeInOut' }}
className="bg-white rounded-2xl p-8 border border-slate-200"
className="bg-white rounded-2xl p-4 md:p-8 border border-slate-200"
>
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-1">
@ -133,10 +133,10 @@ export function WorkflowCarousel({
aria-label={`Go to step ${index + 1}`}
aria-current={activeStep === index ? 'step' : undefined}
className={cn(
'rounded-full transition-all duration-300',
'rounded-full transition-all duration-300 p-2',
activeStep === index
? 'w-4 h-4 bg-primary'
: 'w-3 h-3 bg-slate-600 hover:bg-slate-500'
? 'w-5 h-5 bg-primary'
: 'w-4 h-4 bg-slate-600 hover:bg-slate-500'
)}
/>
))}

View File

@ -1,4 +1,4 @@
import { render, screen, act } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { AnimatedSection } from './AnimatedSection'

View File

@ -1,8 +1,8 @@
import { motion } from 'framer-motion'
import { Children, cloneElement, isValidElement } from 'react'
import { Children, isValidElement } from 'react'
import { cn } from '../../lib/utils'
import { useAnimateOnScroll } from '../../hooks/useAnimateOnScroll'
import { animationVariants, staggerContainer, type AnimationVariant } from '../../utils/animations'
import { animationVariants, type AnimationVariant } from '../../utils/animations'
interface AnimatedSectionProps {
children: React.ReactNode

View File

@ -22,12 +22,13 @@ describe('Card', () => {
);
const card = screen.getByTestId('card');
// Base styles: white bg, border, rounded-2xl, p-8, shadow-md
// Base styles: white bg, border, rounded-2xl, responsive padding, shadow-md
expect(card).toHaveClass('bg-white');
expect(card).toHaveClass('border');
expect(card).toHaveClass('border-border');
expect(card).toHaveClass('rounded-2xl');
expect(card).toHaveClass('p-8');
expect(card).toHaveClass('p-4');
expect(card).toHaveClass('md:p-8');
expect(card).toHaveClass('shadow-md');
});

View File

@ -23,7 +23,7 @@ export function Card({
onClick,
'data-testid': testId,
}: CardProps) {
const baseStyles = 'bg-white border border-border rounded-2xl p-8 shadow-md transition-all duration-300 ease-in-out'
const baseStyles = 'bg-white border border-border rounded-2xl p-4 md:p-8 shadow-md transition-all duration-300 ease-in-out'
const hoverStyles = hover ? 'hover:-translate-y-1.5 hover:shadow-xl' : ''