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

16
.dockerignore Normal file
View File

@ -0,0 +1,16 @@
node_modules
dist
.git
.gitea
*.md
!README.md
.env*
.env.local
coverage
.vscode
.idea
*.log
.DS_Store
Thumbs.db
plan/
.claude/

View File

@ -1,82 +1,104 @@
name: Deploy Website
name: Build and Deploy Website
run-name: Deploying website changes by ${{ github.actor }}
on:
push:
branches: [ main ]
paths:
- '*.html'
- 'assets/**'
- 'css/**'
- 'js/**'
- 'images/**'
paths:
- 'src/**'
- 'public/**'
- 'package*.json'
- 'vite.config.ts'
- 'tailwind.config.ts'
- 'tsconfig*.json'
- 'index.html'
- 'Dockerfile'
- 'nginx.conf'
- 'docker-compose.yml'
workflow_dispatch:
jobs:
deploy:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Validate HTML
run: |
apt-get update && apt-get install -y tidy
find . -name "*.html" -exec tidy -q -e {} \;
echo "✅ HTML validation passed"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint
run: npm run lint
- name: Run tests
run: npm run test -- --run
- name: Build production
run: npm run build
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
echo "Host *" > ~/.ssh/config
echo " StrictHostKeyChecking no" >> ~/.ssh/config
echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config
chmod 600 ~/.ssh/config
- name: Deploy website
- name: Deploy to server
run: |
# Install rsync
echo "📦 Installing rsync..."
apt-get update && apt-get install -y rsync
SERVER_IP="192.168.4.56"
DEPLOY_USER="deploy"
WEBSITE_DIR="/media/stephen/Storage_Linux/infrastructure/services/websites/maverickApplications"
echo "📁 Syncing website files to server..."
echo "Syncing project files to server..."
rsync -avz --delete \
--no-times \
--no-perms \
--exclude='.git' \
--exclude='node_modules' \
--exclude='.gitea' \
--exclude='README.md' \
--exclude='docker-compose.yml' \
--exclude='plan' \
--exclude='.claude' \
-e "ssh -i ~/.ssh/deploy_key" \
./ $DEPLOY_USER@$SERVER_IP:$WEBSITE_DIR/
echo "🔄 Restarting website container..."
echo "Building Docker image on server..."
ssh -i ~/.ssh/deploy_key $DEPLOY_USER@$SERVER_IP \
"cd $WEBSITE_DIR && docker compose restart maverick-website"
"cd $WEBSITE_DIR && docker compose build --no-cache"
echo "Starting updated container..."
ssh -i ~/.ssh/deploy_key $DEPLOY_USER@$SERVER_IP \
"cd $WEBSITE_DIR && docker compose up -d"
- name: Health check
run: |
echo "🏥 Waiting for container to restart..."
sleep 15
echo "Waiting for container to be healthy..."
sleep 20
for i in {1..5}; do
if curl -f --connect-timeout 10 https://maverickapplications.com; then
echo "✅ Website is responding correctly!"
echo "Website is responding correctly!"
exit 0
fi
echo "⏳ Attempt $i failed, retrying in 10 seconds..."
echo "Attempt $i failed, retrying in 10 seconds..."
sleep 10
done
echo "❌ Website health check failed"
echo "Website health check failed"
exit 1
- name: Cleanup
if: always()
run: rm -f ~/.ssh/deploy_key
run: rm -f ~/.ssh/deploy_key

4
.gitignore vendored
View File

@ -22,3 +22,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Original site backup
index.html.original
index.html.backup*

39
Dockerfile Normal file
View File

@ -0,0 +1,39 @@
# =============================================================================
# Stage 1: Build
# =============================================================================
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files first (for layer caching)
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# =============================================================================
# Stage 2: Production
# =============================================================================
FROM nginx:alpine AS production
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy built assets from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port 80
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
# Start nginx
CMD ["nginx", "-g", "daemon off;"]

View File

@ -1,18 +1,25 @@
services:
maverick-website:
image: nginx:alpine
build:
context: .
dockerfile: Dockerfile
image: maverick-website:latest
container_name: maverick-website
restart: unless-stopped
volumes:
- ./:/usr/share/nginx/html:ro
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.maverick-website.rule=Host(`maverickapplications.com`)"
- "traefik.http.routers.maverick-website.tls=true"
- "traefik.http.routers.maverick-website.tls.certresolver=letsencrypt"
- "traefik.http.routers.maverick-website.entrypoints=web"
- "traefik.http.routers.maverick-website.middlewares=cloudflare-headers@file"
- "traefik.http.services.maverick-website.loadbalancer.server.port=80"
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
networks:
traefik:

View File

@ -1,756 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Maverick Applications - Custom Software Solutions</title>
<meta name="description" content="Professional software development and custom application solutions. Built with precision, delivered with excellence.">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--secondary: #64748b;
--accent: #f59e0b;
--text-primary: #1e293b;
--text-secondary: #64748b;
--bg-primary: #ffffff;
--bg-secondary: #f8fafc;
--border: #e2e8f0;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-primary);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
/* Header */
header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
transition: all 0.3s ease;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logo::before {
content: "◆";
font-size: 1.2rem;
color: var(--accent);
}
.nav-links {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-links a {
text-decoration: none;
color: var(--text-secondary);
font-weight: 500;
transition: color 0.3s ease;
position: relative;
}
.nav-links a:hover {
color: var(--primary);
}
.nav-links a::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 0;
height: 2px;
background: var(--primary);
transition: width 0.3s ease;
}
.nav-links a:hover::after {
width: 100%;
}
/* Hero Section */
.hero {
background: linear-gradient(135deg, var(--bg-secondary) 0%, #f1f5f9 100%);
padding: 8rem 0 6rem;
position: relative;
overflow: hidden;
}
.hero::before {
content: '';
position: absolute;
top: 0;
right: -50%;
width: 100%;
height: 100%;
background: linear-gradient(135deg, transparent 0%, rgba(37, 99, 235, 0.05) 100%);
transform: skewX(-15deg);
}
.hero-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: center;
position: relative;
z-index: 2;
}
.hero-text h1 {
font-size: 3.5rem;
font-weight: 800;
line-height: 1.1;
margin-bottom: 1.5rem;
background: linear-gradient(135deg, var(--text-primary) 0%, var(--primary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-text p {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2rem;
max-width: 500px;
}
.cta-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.btn {
padding: 0.875rem 2rem;
border-radius: 0.5rem;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
border: none;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--primary);
color: white;
box-shadow: 0 4px 14px 0 rgba(37, 99, 235, 0.39);
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 8px 25px 0 rgba(37, 99, 235, 0.5);
}
.btn-secondary {
background: transparent;
color: var(--primary);
border: 2px solid var(--primary);
}
.btn-secondary:hover {
background: var(--primary);
color: white;
transform: translateY(-2px);
}
.hero-visual {
display: flex;
justify-content: center;
align-items: center;
}
.tech-stack {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
max-width: 400px;
}
.tech-item {
background: white;
padding: 1.5rem;
border-radius: 1rem;
text-align: center;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
border: 1px solid var(--border);
}
.tech-item:hover {
transform: translateY(-5px);
}
.tech-item::before {
content: attr(data-icon);
font-size: 2rem;
display: block;
margin-bottom: 0.5rem;
}
/* Services Section */
.services {
padding: 6rem 0;
background: var(--bg-primary);
}
.section-header {
text-align: center;
margin-bottom: 4rem;
}
.section-header h2 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--text-primary);
}
.section-header p {
font-size: 1.125rem;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
.service-card {
background: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.service-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--accent));
}
.service-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
.service-icon {
font-size: 3rem;
margin-bottom: 1rem;
display: block;
}
.service-card h3 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-primary);
}
.service-card p {
color: var(--text-secondary);
line-height: 1.6;
}
/* Stats Section */
.stats {
background: var(--primary);
padding: 4rem 0;
color: white;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 2rem;
text-align: center;
}
.stat-item h3 {
font-size: 3rem;
font-weight: 800;
margin-bottom: 0.5rem;
}
.stat-item p {
font-size: 1.125rem;
opacity: 0.9;
}
/* Contact Section */
.contact {
padding: 6rem 0;
background: var(--bg-secondary);
}
.contact-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: start;
}
.contact-info h3 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--text-primary);
}
.contact-info p {
color: var(--text-secondary);
margin-bottom: 2rem;
}
.contact-methods {
display: flex;
flex-direction: column;
gap: 1rem;
}
.contact-method {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: white;
border-radius: 0.5rem;
border: 1px solid var(--border);
}
.contact-method span {
font-size: 1.5rem;
}
.contact-form {
background: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border);
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-primary);
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
font-size: 1rem;
transition: border-color 0.3s ease;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.form-group textarea {
height: 120px;
resize: vertical;
}
/* Footer */
footer {
background: var(--text-primary);
color: white;
padding: 2rem 0;
text-align: center;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
}
.footer-links {
display: flex;
gap: 2rem;
}
.footer-links a {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
transition: color 0.3s ease;
}
.footer-links a:hover {
color: white;
}
/* Responsive Design */
@media (max-width: 768px) {
.container {
padding: 0 1rem;
}
.hero {
padding: 6rem 0 4rem;
}
.hero-content {
grid-template-columns: 1fr;
gap: 3rem;
text-align: center;
}
.hero-text h1 {
font-size: 2.5rem;
}
.nav-links {
display: none;
}
.contact-content {
grid-template-columns: 1fr;
gap: 2rem;
}
.cta-buttons {
justify-content: center;
}
.footer-content {
flex-direction: column;
text-align: center;
}
}
/* Animations */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeInUp 0.6s ease-out;
}
/* Smooth scroll */
html {
scroll-behavior: smooth;
}
</style>
</head>
<body>
<header>
<nav class="container">
<a href="#" class="logo">Maverick Applications</a>
<ul class="nav-links">
<li><a href="#home">Home</a></li>
<li><a href="#services">Services</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
</header>
<main>
<section id="home" class="hero">
<div class="container">
<div class="hero-content">
<div class="hero-text animate-fade-in">
<h1>Custom Software Solutions</h1>
<p>Building robust, scalable applications with cutting-edge technology. From concept to deployment, we deliver excellence in every line of code.</p>
<div class="cta-buttons">
<a href="#contact" class="btn btn-primary">Get Started</a>
<a href="#services" class="btn btn-secondary">Our Services</a>
</div>
</div>
<div class="hero-visual">
<div class="tech-stack">
<div class="tech-item" data-icon="⚡">
<strong>Fast</strong>
</div>
<div class="tech-item" data-icon="🔒">
<strong>Secure</strong>
</div>
<div class="tech-item" data-icon="📱">
<strong>Modern</strong>
</div>
<div class="tech-item" data-icon="☁️">
<strong>Cloud</strong>
</div>
<div class="tech-item" data-icon="🚀">
<strong>Scalable</strong>
</div>
<div class="tech-item" data-icon="🎯">
<strong>Precise</strong>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="services" class="services">
<div class="container">
<div class="section-header">
<h2>Our Services</h2>
<p>Comprehensive software development solutions tailored to your business needs</p>
</div>
<div class="services-grid">
<div class="service-card">
<span class="service-icon">💻</span>
<h3>Web Applications</h3>
<p>Full-stack web development using modern frameworks and technologies. Responsive, fast, and user-friendly applications that scale with your business.</p>
</div>
<div class="service-card">
<span class="service-icon">📱</span>
<h3>Mobile Development</h3>
<p>Native and cross-platform mobile applications for iOS and Android. Beautiful interfaces with seamless user experiences.</p>
</div>
<div class="service-card">
<span class="service-icon">🔧</span>
<h3>DevOps & Infrastructure</h3>
<p>Server setup, containerization, CI/CD pipelines, and cloud deployment. Like our Traefik-powered infrastructure for optimal performance.</p>
</div>
<div class="service-card">
<span class="service-icon">🔍</span>
<h3>System Integration</h3>
<p>Connecting disparate systems and APIs to create unified solutions. Data synchronization and workflow automation.</p>
</div>
<div class="service-card">
<span class="service-icon">🛡️</span>
<h3>Security Solutions</h3>
<p>Implementing robust security measures, authentication systems, and compliance frameworks to protect your digital assets.</p>
</div>
<div class="service-card">
<span class="service-icon">📊</span>
<h3>Data Analytics</h3>
<p>Transform your data into actionable insights with custom dashboards, reporting tools, and business intelligence solutions.</p>
</div>
</div>
</div>
</section>
<section class="stats">
<div class="container">
<div class="stats-grid">
<div class="stat-item">
<h3>100+</h3>
<p>Projects Delivered</p>
</div>
<div class="stat-item">
<h3>99.9%</h3>
<p>Uptime</p>
</div>
<div class="stat-item">
<h3>24/7</h3>
<p>Support</p>
</div>
<div class="stat-item">
<h3>5⭐</h3>
<p>Client Rating</p>
</div>
</div>
</div>
</section>
<section id="contact" class="contact">
<div class="container">
<div class="section-header">
<h2>Get In Touch</h2>
<p>Ready to start your next project? Let's discuss how we can help bring your ideas to life.</p>
</div>
<div class="contact-content">
<div class="contact-info">
<h3>Let's Work Together</h3>
<p>We're always excited to take on new challenges and help businesses grow through innovative software solutions.</p>
<div class="contact-methods">
<div class="contact-method">
<span>📧</span>
<div>
<strong>Email</strong>
<p>hello@maverickapplications.com</p>
</div>
</div>
<div class="contact-method">
<span>💬</span>
<div>
<strong>Response Time</strong>
<p>Within 24 hours</p>
</div>
</div>
<div class="contact-method">
<span>🌐</span>
<div>
<strong>Location</strong>
<p>Worldwide Remote</p>
</div>
</div>
</div>
</div>
<form class="contact-form" onsubmit="handleSubmit(event)">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="project">Project Type</label>
<input type="text" id="project" name="project" placeholder="Web App, Mobile App, etc.">
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" placeholder="Tell us about your project..." required></textarea>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">Send Message</button>
</form>
</div>
</div>
</section>
</main>
<footer>
<div class="container">
<div class="footer-content">
<div class="logo">Maverick Applications</div>
<div class="footer-links">
<a href="#home">Home</a>
<a href="#services">Services</a>
<a href="#contact">Contact</a>
</div>
<p>&copy; 2025 Maverick Applications. All rights reserved.</p>
</div>
</div>
</footer>
<script>
// Smooth scrolling for navigation links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Header scroll effect
window.addEventListener('scroll', () => {
const header = document.querySelector('header');
if (window.scrollY > 100) {
header.style.background = 'rgba(255, 255, 255, 0.98)';
header.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
} else {
header.style.background = 'rgba(255, 255, 255, 0.95)';
header.style.boxShadow = 'none';
}
});
// Form submission handler
function handleSubmit(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
// Show success message
alert('Thank you for your message! We\'ll get back to you within 24 hours.');
form.reset();
}
// Animate elements on scroll
const observerOptions = {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-fade-in');
}
});
}, observerOptions);
// Observe service cards and other elements
document.querySelectorAll('.service-card, .stat-item').forEach(el => {
observer.observe(el);
});
</script>
</body>
</html>
<!-- Last updated: $(date) -->

60
nginx.conf Normal file
View File

@ -0,0 +1,60 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cache static assets aggressively (JS, CSS, images)
# Vite adds content hashes to filenames, so long cache is safe
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# Don't cache HTML (for updates to take effect)
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}
# SPA fallback - serve index.html for all routes
location / {
try_files $uri $uri/ /index.html;
}
# Deny access to hidden files (except .well-known)
location ~ /\.(?!well-known) {
deny all;
}
# Error pages
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
internal;
}
}

28
package-lock.json generated
View File

@ -13,7 +13,8 @@
"lucide-react": "^0.400.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^2.2.0"
"tailwind-merge": "^2.2.0",
"zod-validation-error": "^4.0.2"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@ -28,7 +29,7 @@
"autoprefixer": "^10.4.24",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^9.39.1",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
@ -3251,6 +3252,19 @@
"eslint": ">=7"
}
},
"node_modules/eslint-plugin-react-compiler/node_modules/zod-validation-error": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz",
"integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.24.4"
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
@ -6556,23 +6570,21 @@
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-validation-error": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz",
"integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==",
"dev": true,
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
"integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.24.4"
"zod": "^3.25.0 || ^4.0.0"
}
}
}

View File

@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"type-check": "tsc --noEmit",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest",
@ -18,7 +19,8 @@
"lucide-react": "^0.400.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^2.2.0"
"tailwind-merge": "^2.2.0",
"zod-validation-error": "^4.0.2"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@ -33,7 +35,7 @@
"autoprefixer": "^10.4.24",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"eslint": "^9.39.1",
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

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' : ''