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:
@ -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
|
||||
|
||||
@ -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} />)
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
});
|
||||
|
||||
|
||||
@ -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' : ''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user