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>
16
.dockerignore
Normal 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/
|
||||||
@ -1,82 +1,104 @@
|
|||||||
name: Deploy Website
|
name: Build and Deploy Website
|
||||||
run-name: Deploying website changes by ${{ github.actor }}
|
run-name: Deploying website changes by ${{ github.actor }}
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
paths:
|
||||||
- '*.html'
|
- 'src/**'
|
||||||
- 'assets/**'
|
- 'public/**'
|
||||||
- 'css/**'
|
- 'package*.json'
|
||||||
- 'js/**'
|
- 'vite.config.ts'
|
||||||
- 'images/**'
|
- 'tailwind.config.ts'
|
||||||
|
- 'tsconfig*.json'
|
||||||
|
- 'index.html'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- 'nginx.conf'
|
||||||
|
- 'docker-compose.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
build-and-deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate HTML
|
- name: Setup Node.js
|
||||||
run: |
|
uses: actions/setup-node@v4
|
||||||
apt-get update && apt-get install -y tidy
|
with:
|
||||||
find . -name "*.html" -exec tidy -q -e {} \;
|
node-version: '20'
|
||||||
echo "✅ HTML validation passed"
|
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
|
- name: Setup SSH
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key
|
||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
echo "Host *" > ~/.ssh/config
|
echo "Host *" > ~/.ssh/config
|
||||||
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
echo " StrictHostKeyChecking no" >> ~/.ssh/config
|
||||||
echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config
|
echo " UserKnownHostsFile /dev/null" >> ~/.ssh/config
|
||||||
chmod 600 ~/.ssh/config
|
chmod 600 ~/.ssh/config
|
||||||
|
|
||||||
- name: Deploy website
|
- name: Deploy to server
|
||||||
run: |
|
run: |
|
||||||
# Install rsync
|
|
||||||
echo "📦 Installing rsync..."
|
|
||||||
apt-get update && apt-get install -y rsync
|
apt-get update && apt-get install -y rsync
|
||||||
|
|
||||||
SERVER_IP="192.168.4.56"
|
SERVER_IP="192.168.4.56"
|
||||||
DEPLOY_USER="deploy"
|
DEPLOY_USER="deploy"
|
||||||
WEBSITE_DIR="/media/stephen/Storage_Linux/infrastructure/services/websites/maverickApplications"
|
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 \
|
rsync -avz --delete \
|
||||||
--no-times \
|
|
||||||
--no-perms \
|
|
||||||
--exclude='.git' \
|
--exclude='.git' \
|
||||||
|
--exclude='node_modules' \
|
||||||
--exclude='.gitea' \
|
--exclude='.gitea' \
|
||||||
--exclude='README.md' \
|
--exclude='plan' \
|
||||||
--exclude='docker-compose.yml' \
|
--exclude='.claude' \
|
||||||
-e "ssh -i ~/.ssh/deploy_key" \
|
-e "ssh -i ~/.ssh/deploy_key" \
|
||||||
./ $DEPLOY_USER@$SERVER_IP:$WEBSITE_DIR/
|
./ $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 \
|
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
|
- name: Health check
|
||||||
run: |
|
run: |
|
||||||
echo "🏥 Waiting for container to restart..."
|
echo "Waiting for container to be healthy..."
|
||||||
sleep 15
|
sleep 20
|
||||||
|
|
||||||
for i in {1..5}; do
|
for i in {1..5}; do
|
||||||
if curl -f --connect-timeout 10 https://maverickapplications.com; then
|
if curl -f --connect-timeout 10 https://maverickapplications.com; then
|
||||||
echo "✅ Website is responding correctly!"
|
echo "Website is responding correctly!"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
echo "⏳ Attempt $i failed, retrying in 10 seconds..."
|
echo "Attempt $i failed, retrying in 10 seconds..."
|
||||||
sleep 10
|
sleep 10
|
||||||
done
|
done
|
||||||
echo "❌ Website health check failed"
|
|
||||||
|
echo "Website health check failed"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
if: always()
|
if: always()
|
||||||
run: rm -f ~/.ssh/deploy_key
|
run: rm -f ~/.ssh/deploy_key
|
||||||
|
|||||||
4
.gitignore
vendored
@ -22,3 +22,7 @@ dist-ssr
|
|||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Original site backup
|
||||||
|
index.html.original
|
||||||
|
index.html.backup*
|
||||||
|
|||||||
39
Dockerfile
Normal 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;"]
|
||||||
@ -1,18 +1,25 @@
|
|||||||
services:
|
services:
|
||||||
maverick-website:
|
maverick-website:
|
||||||
image: nginx:alpine
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: maverick-website:latest
|
||||||
container_name: maverick-website
|
container_name: maverick-website
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
|
||||||
- ./:/usr/share/nginx/html:ro
|
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- traefik
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.maverick-website.rule=Host(`maverickapplications.com`)"
|
- "traefik.http.routers.maverick-website.rule=Host(`maverickapplications.com`)"
|
||||||
- "traefik.http.routers.maverick-website.tls=true"
|
- "traefik.http.routers.maverick-website.entrypoints=web"
|
||||||
- "traefik.http.routers.maverick-website.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.maverick-website.middlewares=cloudflare-headers@file"
|
||||||
- "traefik.http.services.maverick-website.loadbalancer.server.port=80"
|
- "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:
|
networks:
|
||||||
traefik:
|
traefik:
|
||||||
|
|||||||
@ -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>© 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
@ -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
@ -13,7 +13,8 @@
|
|||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@ -28,7 +29,7 @@
|
|||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||||
"eslint": "^9.39.1",
|
"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-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
@ -3251,6 +3252,19 @@
|
|||||||
"eslint": ">=7"
|
"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": {
|
"node_modules/eslint-plugin-react-hooks": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
|
"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",
|
"version": "3.25.76",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-validation-error": {
|
"node_modules/zod-validation-error": {
|
||||||
"version": "3.5.4",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
|
||||||
"integrity": "sha512-+hEiRIiPobgyuFlEojnqjJnhFvg4r/i3cqgcm67eehZf/WBaK3g6cD02YU9mtdVxZjv8CzCA9n/Rhrs3yAAvAw==",
|
"integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
@ -18,7 +19,8 @@
|
|||||||
"lucide-react": "^0.400.0",
|
"lucide-react": "^0.400.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
@ -33,7 +35,7 @@
|
|||||||
"autoprefixer": "^10.4.24",
|
"autoprefixer": "^10.4.24",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||||
"eslint": "^9.39.1",
|
"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-hooks": "^7.0.1",
|
||||||
"eslint-plugin-react-refresh": "^0.4.24",
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
"globals": "^16.5.0",
|
"globals": "^16.5.0",
|
||||||
|
|||||||
BIN
public/images/contact-collab.webp
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/images/hero-dashboard.webp
Normal file
|
After Width: | Height: | Size: 6.9 MiB |
BIN
public/images/service-devops.webp
Normal file
|
After Width: | Height: | Size: 6.6 MiB |
BIN
public/images/service-mobile.webp
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
public/images/service-web-apps.webp
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
public/images/workflow-build.webp
Normal file
|
After Width: | Height: | Size: 5.8 MiB |
BIN
public/images/workflow-deploy.webp
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
public/images/workflow-design.webp
Normal file
|
After Width: | Height: | Size: 6.0 MiB |
BIN
public/images/workflow-discovery.webp
Normal file
|
After Width: | Height: | Size: 6.3 MiB |
@ -36,8 +36,6 @@ export function BentoCard({
|
|||||||
'data-testid': testId,
|
'data-testid': testId,
|
||||||
}: BentoCardProps) {
|
}: BentoCardProps) {
|
||||||
const Icon = iconMap[service.icon] || Globe
|
const Icon = iconMap[service.icon] || Globe
|
||||||
const showDetails = size === 'lg' || size === 'md'
|
|
||||||
const showTechnologies = size === 'lg'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -149,7 +149,7 @@ describe('ExpandableServiceCard', () => {
|
|||||||
it('works without technologies prop', async () => {
|
it('works without technologies prop', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
const propsWithoutTech = { ...defaultProps }
|
const propsWithoutTech = { ...defaultProps }
|
||||||
delete (propsWithoutTech as any).technologies
|
delete (propsWithoutTech as Record<string, unknown>).technologies
|
||||||
|
|
||||||
render(<ExpandableServiceCard {...propsWithoutTech} />)
|
render(<ExpandableServiceCard {...propsWithoutTech} />)
|
||||||
|
|
||||||
|
|||||||
@ -120,7 +120,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-12 lg:grid-cols-2 lg:gap-16">
|
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16">
|
||||||
{/* Contact Info */}
|
{/* Contact Info */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export function WorkflowCarousel({
|
|||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 1 }}
|
exit={{ opacity: 1 }}
|
||||||
transition={{ duration: 1.0, ease: 'easeInOut' }}
|
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 flex-col md:flex-row gap-8">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@ -133,10 +133,10 @@ export function WorkflowCarousel({
|
|||||||
aria-label={`Go to step ${index + 1}`}
|
aria-label={`Go to step ${index + 1}`}
|
||||||
aria-current={activeStep === index ? 'step' : undefined}
|
aria-current={activeStep === index ? 'step' : undefined}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-full transition-all duration-300',
|
'rounded-full transition-all duration-300 p-2',
|
||||||
activeStep === index
|
activeStep === index
|
||||||
? 'w-4 h-4 bg-primary'
|
? 'w-5 h-5 bg-primary'
|
||||||
: 'w-3 h-3 bg-slate-600 hover:bg-slate-500'
|
: '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 { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
import { AnimatedSection } from './AnimatedSection'
|
import { AnimatedSection } from './AnimatedSection'
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Children, cloneElement, isValidElement } from 'react'
|
import { Children, isValidElement } from 'react'
|
||||||
import { cn } from '../../lib/utils'
|
import { cn } from '../../lib/utils'
|
||||||
import { useAnimateOnScroll } from '../../hooks/useAnimateOnScroll'
|
import { useAnimateOnScroll } from '../../hooks/useAnimateOnScroll'
|
||||||
import { animationVariants, staggerContainer, type AnimationVariant } from '../../utils/animations'
|
import { animationVariants, type AnimationVariant } from '../../utils/animations'
|
||||||
|
|
||||||
interface AnimatedSectionProps {
|
interface AnimatedSectionProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|||||||
@ -22,12 +22,13 @@ describe('Card', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const card = screen.getByTestId('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('bg-white');
|
||||||
expect(card).toHaveClass('border');
|
expect(card).toHaveClass('border');
|
||||||
expect(card).toHaveClass('border-border');
|
expect(card).toHaveClass('border-border');
|
||||||
expect(card).toHaveClass('rounded-2xl');
|
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');
|
expect(card).toHaveClass('shadow-md');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export function Card({
|
|||||||
onClick,
|
onClick,
|
||||||
'data-testid': testId,
|
'data-testid': testId,
|
||||||
}: CardProps) {
|
}: 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' : ''
|
const hoverStyles = hover ? 'hover:-translate-y-1.5 hover:shadow-xl' : ''
|
||||||
|
|
||||||
|
|||||||