fully commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# OmniVerse Production Environment Configuration
|
||||
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
|
||||
# News Crawler Service
|
||||
PORT=3001
|
||||
API_KEY=6b212515bedb521bc700e125a8d43ae53907a5160296abdab2064aaa3ed86526
|
||||
ENABLE_SCHEDULER=true
|
||||
CRAWL_INTERVAL_MINUTES=30
|
||||
MAX_NEWS_AGE_DAYS=7
|
||||
|
||||
# Database
|
||||
DATABASE_PATH=./data/news.db
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=./logs/app.log
|
||||
|
||||
# Cache
|
||||
CACHE_TTL_SECONDS=300
|
||||
|
||||
# CORS (允许的前端域名)
|
||||
CORS_ORIGIN=https://www.omniverseai.net,https://omniverseai.net,http://localhost:3000
|
||||
@@ -0,0 +1,24 @@
|
||||
# OmniVerse Production Environment Configuration
|
||||
|
||||
# Application
|
||||
NODE_ENV=production
|
||||
|
||||
# News Crawler Service
|
||||
PORT=3001
|
||||
API_KEY=your-secure-api-key-here-change-this
|
||||
ENABLE_SCHEDULER=true
|
||||
CRAWL_INTERVAL_MINUTES=30
|
||||
MAX_NEWS_AGE_DAYS=7
|
||||
|
||||
# Database
|
||||
DATABASE_PATH=./data/news.db
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=./logs/app.log
|
||||
|
||||
# Cache
|
||||
CACHE_TTL_SECONDS=300
|
||||
|
||||
# CORS (允许的前端域名)
|
||||
CORS_ORIGIN=https://www.omniverseai.net,https://omniverseai.net,http://localhost:3000
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Deploy Website to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
- 'docs/**'
|
||||
- '.gitignore'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HARBOR_REGISTRY: harbor.omniverseai.net
|
||||
HARBOR_PROJECT: omniverse
|
||||
IMAGE_NAME: website
|
||||
KUBECONFIG: /etc/rancher/k3s/k3s.yaml
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: [self-hosted, Linux, X64]
|
||||
|
||||
steps:
|
||||
# ========================================
|
||||
# 1. Checkout Code
|
||||
# ========================================
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# ========================================
|
||||
# 2. Set up Docker Buildx
|
||||
# ========================================
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# ========================================
|
||||
# 3. Login to Harbor
|
||||
# ========================================
|
||||
- name: Login to Harbor
|
||||
run: |
|
||||
echo "${{ secrets.HARBOR_PASSWORD }}" | docker login ${{ env.HARBOR_REGISTRY }} -u ${{ secrets.HARBOR_USERNAME }} --password-stdin
|
||||
|
||||
# ========================================
|
||||
# 4. Build and Push Docker Image
|
||||
# ========================================
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
IMAGE_TAG="${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest"
|
||||
IMAGE_SHA="${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ github.sha }}"
|
||||
|
||||
docker build -f Dockerfile.prod -t $IMAGE_TAG -t $IMAGE_SHA .
|
||||
docker push $IMAGE_TAG
|
||||
docker push $IMAGE_SHA
|
||||
|
||||
echo "Built and pushed: $IMAGE_TAG"
|
||||
echo "Built and pushed: $IMAGE_SHA"
|
||||
|
||||
# ========================================
|
||||
# 5. Deploy to k3s
|
||||
# ========================================
|
||||
- name: Deploy to k3s
|
||||
env:
|
||||
KUBECONFIG: ${{ env.KUBECONFIG }}
|
||||
run: |
|
||||
# Apply Kubernetes manifests
|
||||
kubectl apply -f k8s/production.yaml
|
||||
|
||||
# Force pull latest image and restart
|
||||
kubectl set image deployment/website website=${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:${{ github.sha }} -n prod
|
||||
kubectl rollout restart deployment/website -n prod
|
||||
|
||||
# Wait for rollout to complete
|
||||
kubectl rollout status deployment/website -n prod --timeout=300s
|
||||
|
||||
# ========================================
|
||||
# 6. Verify Deployment
|
||||
# ========================================
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
echo "==================================="
|
||||
echo "Website Deployment Summary"
|
||||
echo "==================================="
|
||||
echo ""
|
||||
echo "Image: ${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/${{ env.IMAGE_NAME }}:latest"
|
||||
echo "Commit: ${{ github.sha }}"
|
||||
echo "Namespace: prod"
|
||||
echo ""
|
||||
echo "Pods:"
|
||||
kubectl get pods -n prod -l app=website
|
||||
echo ""
|
||||
echo "Service:"
|
||||
kubectl get svc -n prod website-svc
|
||||
echo ""
|
||||
echo "Ingress:"
|
||||
kubectl get ingress -n prod website-ingress
|
||||
echo "==================================="
|
||||
@@ -0,0 +1,95 @@
|
||||
# 网站空白页面问题修复指南
|
||||
|
||||
## 问题诊断
|
||||
|
||||
您的网站已成功部署在服务器上,但通过 https://www.omniverseai.net 访问时看到空白页面。
|
||||
|
||||
经过检查:
|
||||
- ✅ 服务器正常运行
|
||||
- ✅ HTML 文件正确返回
|
||||
- ✅ JS/CSS 资源文件可以正常访问
|
||||
- ⚠️ **问题原因:Cloudflare CDN 缓存**
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案一:清除 Cloudflare 缓存(推荐)
|
||||
|
||||
1. 登录 Cloudflare 控制台:https://dash.cloudflare.com
|
||||
2. 选择域名 `omniverseai.net`
|
||||
3. 进入 **"缓存" (Caching)** 菜单
|
||||
4. 点击 **"清除所有文件" (Purge Everything)**
|
||||
5. 确认清除
|
||||
6. 等待 30 秒后,刷新浏览器(按 Ctrl+Shift+R 强制刷新)
|
||||
|
||||
### 方案二:临时禁用 Cloudflare 代理
|
||||
|
||||
如果清除缓存后仍有问题,可以临时禁用 Cloudflare:
|
||||
|
||||
1. 登录 Cloudflare 控制台
|
||||
2. 进入 **DNS 设置**
|
||||
3. 找到 `www` 和 `@` 记录
|
||||
4. 点击橙色云图标,将其变为灰色(DNS Only 模式)
|
||||
5. 等待 DNS 传播(约 5-10 分钟)
|
||||
6. 刷新浏览器测试
|
||||
|
||||
### 方案三:配置 Cloudflare 缓存规则
|
||||
|
||||
为了避免未来出现类似问题:
|
||||
|
||||
1. 进入 Cloudflare 的 **"规则" (Rules)** → **"页面规则" (Page Rules)**
|
||||
2. 创建新规则:
|
||||
- URL: `www.omniverseai.net/*`
|
||||
- 设置:
|
||||
- 缓存级别:Standard
|
||||
- Browser Cache TTL: 4 hours
|
||||
- Always Online: ON
|
||||
3. 保存规则
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. 打开浏览器开发者工具(F12)
|
||||
2. 切换到 "Console" 标签
|
||||
3. 访问 https://www.omniverseai.net
|
||||
4. 检查是否有错误信息
|
||||
5. 切换到 "Network" 标签
|
||||
6. 确认 JS 和 CSS 文件是否成功加载(状态码 200)
|
||||
|
||||
## 本地测试(绕过 Cloudflare)
|
||||
|
||||
如果需要直接测试服务器(绕过 Cloudflare):
|
||||
|
||||
```bash
|
||||
# 在您的本地计算机添加 hosts 文件
|
||||
# Windows: C:\Windows\System32\drivers\etc\hosts
|
||||
# Mac/Linux: /etc/hosts
|
||||
|
||||
154.38.168.199 www.omniverseai.net
|
||||
```
|
||||
|
||||
然后访问 http://www.omniverseai.net(注意是 HTTP)
|
||||
|
||||
## 检查 Cloudflare SSL 设置
|
||||
|
||||
确保 Cloudflare SSL/TLS 设置正确:
|
||||
|
||||
1. 进入 **SSL/TLS** 设置
|
||||
2. 加密模式应设置为:
|
||||
- **Flexible**(如果服务器只有 HTTP)
|
||||
- **Full**(如果服务器有自签名证书)
|
||||
- **Full (strict)**(如果服务器有有效的 SSL 证书)
|
||||
|
||||
**当前服务器配置**:仅 HTTP,因此建议使用 **Flexible** 模式
|
||||
|
||||
## 技术细节
|
||||
|
||||
- 服务器 IP: 154.38.168.199
|
||||
- HTTP 端口: 80
|
||||
- HTTPS 端口: 443(通过 Cloudflare 代理)
|
||||
- 容器状态: 所有服务正常运行
|
||||
|
||||
## 需要帮助?
|
||||
|
||||
如果以上方法都不能解决问题,请提供:
|
||||
1. 浏览器控制台的错误截图
|
||||
2. Network 标签的资源加载状态
|
||||
3. 当前的 Cloudflare SSL/TLS 设置模式
|
||||
+13
-36
@@ -1,64 +1,41 @@
|
||||
# Node.js 应用 Dockerfile - 自动优化版本
|
||||
# 由 docker-builder 生成
|
||||
# OmniVerse Web Application Dockerfile
|
||||
# Production build for US server
|
||||
|
||||
# 构建阶段
|
||||
# Build stage
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
# 清除代理配置,避免网络冲突
|
||||
ENV HTTP_PROXY=""
|
||||
ENV HTTPS_PROXY=""
|
||||
ENV http_proxy=""
|
||||
ENV https_proxy=""
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 使用国内镜像源加速
|
||||
RUN npm config delete proxy && \
|
||||
npm config delete https-proxy && \
|
||||
npm config set registry https://registry.npmmirror.com && \
|
||||
npm config set cache /tmp/.npm --global
|
||||
|
||||
# 复制 package 文件
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
# Install dependencies
|
||||
RUN npm install --frozen-lockfile
|
||||
|
||||
# 复制源代码
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build -- --base=/
|
||||
RUN npm run build
|
||||
|
||||
# 生产阶段
|
||||
# Production stage
|
||||
FROM node:18-alpine AS production
|
||||
|
||||
# 清除代理配置
|
||||
ENV HTTP_PROXY=""
|
||||
ENV HTTPS_PROXY=""
|
||||
ENV http_proxy=""
|
||||
ENV https_proxy=""
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 配置 npm 使用国内镜像源
|
||||
RUN npm config delete proxy && \
|
||||
npm config delete https-proxy && \
|
||||
npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 只安装运行时工具
|
||||
# Install serve for static file serving
|
||||
RUN npm install -g serve
|
||||
|
||||
# 复制构建产物
|
||||
# Copy build artifacts
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./
|
||||
|
||||
# 暴露端口
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# 健康检查
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000 || exit 1
|
||||
|
||||
# 启动应用
|
||||
# Start application
|
||||
CMD ["serve", "-s", "dist", "-l", "3000"]
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# ============================================================================
|
||||
# OmniVerse Website Production Dockerfile
|
||||
# Multi-stage build for optimized image size
|
||||
# ============================================================================
|
||||
|
||||
# Stage 1: Build
|
||||
FROM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production --ignore-scripts || npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build application
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Production
|
||||
FROM nginx:1.25-alpine
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Expose port
|
||||
EXPOSE 80
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -0,0 +1,298 @@
|
||||
# OmniVerse 官网生产环境部署指南
|
||||
|
||||
## 部署架构
|
||||
|
||||
```
|
||||
Internet (80/443)
|
||||
↓
|
||||
Nginx Reverse Proxy (Docker)
|
||||
↓
|
||||
├─→ Frontend App (Port 3000, Docker)
|
||||
└─→ News Crawler API (Port 3001, Docker)
|
||||
```
|
||||
|
||||
## 服务组件
|
||||
|
||||
1. **Nginx** - 反向代理和SSL终止 (80/443端口)
|
||||
2. **Web Frontend** - React应用 (内部3000端口)
|
||||
3. **News Crawler** - 新闻爬虫服务 (内部3001端口)
|
||||
4. **Certbot** - SSL证书自动续期
|
||||
|
||||
## 部署前准备
|
||||
|
||||
### 1. 检查域名DNS解析
|
||||
|
||||
确保域名已正确解析到服务器:
|
||||
```bash
|
||||
nslookup www.omniverseai.net
|
||||
nslookup omniverseai.net
|
||||
```
|
||||
|
||||
### 2. 检查必要端口
|
||||
|
||||
确保以下端口可用:
|
||||
```bash
|
||||
# 检查端口占用
|
||||
netstat -tlnp | grep -E ':(80|443) '
|
||||
|
||||
# 如有其他服务占用,需要先停止
|
||||
```
|
||||
|
||||
### 3. 安装Docker和Docker Compose
|
||||
|
||||
```bash
|
||||
# 检查Docker版本
|
||||
docker --version
|
||||
docker-compose --version
|
||||
|
||||
# 如未安装,请先安装Docker
|
||||
```
|
||||
|
||||
### 4. 配置环境变量
|
||||
|
||||
编辑 `.env.production` 文件,修改必要的配置:
|
||||
```bash
|
||||
nano .env.production
|
||||
```
|
||||
|
||||
重要配置项:
|
||||
- `API_KEY`: 修改为强密码
|
||||
- `CORS_ORIGIN`: 确认域名正确
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 第一步:初始部署(HTTP)
|
||||
|
||||
首次部署使用HTTP模式,用于SSL证书申请:
|
||||
|
||||
```bash
|
||||
# 运行部署脚本
|
||||
./deploy-prod.sh
|
||||
```
|
||||
|
||||
部署脚本会:
|
||||
1. 停止旧容器
|
||||
2. 构建Docker镜像
|
||||
3. 启动所有服务
|
||||
4. 显示服务状态
|
||||
|
||||
### 第二步:验证HTTP访问
|
||||
|
||||
访问 http://www.omniverseai.net 确认网站可以正常访问。
|
||||
|
||||
```bash
|
||||
# 检查服务状态
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose -f docker-compose.prod.yml logs -f web
|
||||
docker-compose -f docker-compose.prod.yml logs -f news-crawler
|
||||
docker-compose -f docker-compose.prod.yml logs -f nginx
|
||||
```
|
||||
|
||||
### 第三步:申请SSL证书
|
||||
|
||||
确认HTTP访问正常后,运行SSL初始化脚本:
|
||||
|
||||
```bash
|
||||
# 申请证书(首次)
|
||||
./init-ssl.sh
|
||||
```
|
||||
|
||||
### 第四步:启用HTTPS
|
||||
|
||||
证书申请成功后,需要更新Nginx配置:
|
||||
|
||||
1. 编辑 `nginx/default.conf`:
|
||||
```bash
|
||||
nano nginx/default.conf
|
||||
```
|
||||
|
||||
2. 进行以下修改:
|
||||
- 在HTTP服务器块中,注释掉临时的 `proxy_pass` 配置
|
||||
- 取消注释 HTTP到HTTPS重定向配置
|
||||
- 取消注释整个HTTPS服务器配置块
|
||||
|
||||
3. 重启Nginx:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml restart nginx
|
||||
```
|
||||
|
||||
4. 验证HTTPS访问:
|
||||
访问 https://www.omniverseai.net
|
||||
|
||||
## 管理命令
|
||||
|
||||
### 查看服务状态
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker-compose -f docker-compose.prod.yml logs -f
|
||||
|
||||
# 查看特定服务日志
|
||||
docker-compose -f docker-compose.prod.yml logs -f web
|
||||
docker-compose -f docker-compose.prod.yml logs -f nginx
|
||||
docker-compose -f docker-compose.prod.yml logs -f news-crawler
|
||||
```
|
||||
|
||||
### 重启服务
|
||||
```bash
|
||||
# 重启所有服务
|
||||
docker-compose -f docker-compose.prod.yml restart
|
||||
|
||||
# 重启特定服务
|
||||
docker-compose -f docker-compose.prod.yml restart web
|
||||
docker-compose -f docker-compose.prod.yml restart nginx
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
```
|
||||
|
||||
### 更新部署
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
git pull
|
||||
|
||||
# 重新部署
|
||||
./deploy-prod.sh
|
||||
```
|
||||
|
||||
### SSL证书续期
|
||||
|
||||
Certbot容器会自动续期证书(每12小时检查一次)。也可以手动续期:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot renew
|
||||
docker-compose -f docker-compose.prod.yml restart nginx
|
||||
```
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 健康检查
|
||||
```bash
|
||||
# Web服务健康检查
|
||||
curl http://localhost:3000
|
||||
|
||||
# News爬虫服务健康检查
|
||||
curl http://localhost:3001/health
|
||||
|
||||
# Nginx状态
|
||||
curl -I http://www.omniverseai.net
|
||||
```
|
||||
|
||||
### 数据备份
|
||||
|
||||
新闻数据库和日志存储在Docker volumes中:
|
||||
```bash
|
||||
# 查看volumes
|
||||
docker volume ls | grep omniverse
|
||||
|
||||
# 备份新闻数据库
|
||||
docker run --rm -v website_news_data:/data -v $(pwd):/backup alpine tar czf /backup/news_data_backup.tar.gz -C /data .
|
||||
|
||||
# 备份日志
|
||||
docker run --rm -v website_news_logs:/logs -v $(pwd):/backup alpine tar czf /backup/news_logs_backup.tar.gz -C /logs .
|
||||
```
|
||||
|
||||
### 清理旧数据
|
||||
|
||||
新闻爬虫服务会自动清理7天前的旧新闻(可在 `.env.production` 中配置)。
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 网站无法访问
|
||||
|
||||
1. 检查服务是否运行:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
```
|
||||
|
||||
2. 检查Nginx日志:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs nginx
|
||||
```
|
||||
|
||||
3. 检查端口是否监听:
|
||||
```bash
|
||||
netstat -tlnp | grep -E ':(80|443) '
|
||||
```
|
||||
|
||||
### SSL证书问题
|
||||
|
||||
1. 检查证书文件:
|
||||
```bash
|
||||
ls -la ./certbot/conf/live/www.omniverseai.net/
|
||||
```
|
||||
|
||||
2. 测试证书:
|
||||
```bash
|
||||
openssl s_client -connect www.omniverseai.net:443 -servername www.omniverseai.net
|
||||
```
|
||||
|
||||
3. 手动续期:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot renew --dry-run
|
||||
```
|
||||
|
||||
### 新闻服务问题
|
||||
|
||||
1. 检查服务日志:
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs news-crawler
|
||||
```
|
||||
|
||||
2. 手动触发爬虫:
|
||||
```bash
|
||||
curl -X POST http://localhost:3001/api/crawl \
|
||||
-H "X-API-Key: your-api-key"
|
||||
```
|
||||
|
||||
3. 检查数据库:
|
||||
```bash
|
||||
docker exec -it omniverse-news-crawler ls -la /app/data
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### Nginx缓存
|
||||
|
||||
可以在 `nginx/default.conf` 中添加缓存配置:
|
||||
```nginx
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;
|
||||
proxy_cache my_cache;
|
||||
```
|
||||
|
||||
### 资源限制
|
||||
|
||||
可以在 `docker-compose.prod.yml` 中添加资源限制:
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '1'
|
||||
memory: 512M
|
||||
```
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. 定期更新Docker镜像
|
||||
2. 使用强密码作为API_KEY
|
||||
3. 启用防火墙,只开放80和443端口
|
||||
4. 定期备份数据
|
||||
5. 监控日志中的异常访问
|
||||
6. 配置fail2ban防止暴力攻击
|
||||
|
||||
## 联系支持
|
||||
|
||||
如遇到问题,请查看:
|
||||
- 应用日志:`docker-compose logs`
|
||||
- Nginx日志:`/var/log/nginx/`(在容器内)
|
||||
- 新闻服务日志:`./news-crawler-service/logs/`
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 生产环境部署脚本
|
||||
# 用于构建和启动OmniVerse官网
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo " OmniVerse 官网生产环境部署"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 检查必要文件
|
||||
if [ ! -f ".env.production" ]; then
|
||||
echo "错误:找不到 .env.production 文件"
|
||||
echo "请复制 .env.production.example 并配置环境变量"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 停止旧容器
|
||||
echo "停止旧容器..."
|
||||
docker-compose -f docker-compose.prod.yml down
|
||||
|
||||
# 清理旧镜像(可选)
|
||||
read -p "是否清理旧的Docker镜像?(y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "清理旧镜像..."
|
||||
docker system prune -f
|
||||
fi
|
||||
|
||||
# 构建镜像
|
||||
echo ""
|
||||
echo "构建Docker镜像..."
|
||||
docker-compose -f docker-compose.prod.yml build --no-cache
|
||||
|
||||
# 启动服务
|
||||
echo ""
|
||||
echo "启动服务..."
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
|
||||
# 显示服务状态
|
||||
echo ""
|
||||
echo "等待服务启动..."
|
||||
sleep 10
|
||||
|
||||
echo ""
|
||||
echo "服务状态:"
|
||||
docker-compose -f docker-compose.prod.yml ps
|
||||
|
||||
# 检查服务健康状态
|
||||
echo ""
|
||||
echo "检查服务健康状态..."
|
||||
sleep 5
|
||||
|
||||
echo ""
|
||||
echo "Web服务健康检查:"
|
||||
docker inspect --format='{{json .State.Health}}' omniverse-web | python3 -m json.tool || echo "健康检查未就绪"
|
||||
|
||||
echo ""
|
||||
echo "News爬虫服务健康检查:"
|
||||
docker inspect --format='{{json .State.Health}}' omniverse-news-crawler | python3 -m json.tool || echo "健康检查未就绪"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 部署完成!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " HTTP: http://www.omniverseai.net"
|
||||
echo " HTTPS: https://www.omniverseai.net (需要先申请SSL证书)"
|
||||
echo ""
|
||||
echo "查看日志:"
|
||||
echo " docker-compose -f docker-compose.prod.yml logs -f"
|
||||
echo ""
|
||||
echo "如需申请SSL证书,请运行:"
|
||||
echo " ./init-ssl.sh"
|
||||
echo ""
|
||||
@@ -0,0 +1,85 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Nginx反向代理
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: omniverse-nginx
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./certbot/conf:/etc/letsencrypt:ro
|
||||
- ./certbot/www:/var/www/certbot:ro
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
- web
|
||||
- news-crawler
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- omniverse-network
|
||||
|
||||
# 前端应用
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: omniverse-web
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- omniverse-network
|
||||
|
||||
# 新闻爬虫服务
|
||||
news-crawler:
|
||||
build:
|
||||
context: ./news-crawler-service
|
||||
dockerfile: Dockerfile
|
||||
container_name: omniverse-news-crawler
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3001
|
||||
volumes:
|
||||
- news_data:/app/data
|
||||
- news_logs:/app/logs
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3001/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
networks:
|
||||
- omniverse-network
|
||||
|
||||
# Certbot用于SSL证书申请和续期
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
container_name: omniverse-certbot
|
||||
volumes:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
|
||||
networks:
|
||||
- omniverse-network
|
||||
|
||||
networks:
|
||||
omniverse-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
news_data:
|
||||
driver: local
|
||||
news_logs:
|
||||
driver: local
|
||||
nginx_logs:
|
||||
driver: local
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
# SSL证书初始化脚本
|
||||
# 用于首次申请Let's Encrypt证书
|
||||
|
||||
set -e
|
||||
|
||||
DOMAIN="www.omniverseai.net"
|
||||
EMAIL="admin@omniverseai.net" # 请修改为您的实际邮箱
|
||||
|
||||
echo "正在为域名 ${DOMAIN} 申请SSL证书..."
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p ./certbot/conf
|
||||
mkdir -p ./certbot/www
|
||||
|
||||
# 启动nginx和certbot服务
|
||||
docker-compose -f docker-compose.prod.yml up -d nginx
|
||||
|
||||
# 等待nginx启动
|
||||
echo "等待Nginx启动..."
|
||||
sleep 5
|
||||
|
||||
# 申请证书
|
||||
echo "申请SSL证书..."
|
||||
docker-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
||||
--webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
--email ${EMAIL} \
|
||||
--agree-tos \
|
||||
--no-eff-email \
|
||||
-d ${DOMAIN} \
|
||||
-d omniverseai.net
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "SSL证书申请成功!"
|
||||
echo ""
|
||||
echo "接下来的步骤:"
|
||||
echo "1. 编辑 nginx/default.conf"
|
||||
echo "2. 取消注释HTTPS服务器配置块"
|
||||
echo "3. 注释或删除HTTP配置中的临时proxy_pass"
|
||||
echo "4. 取消注释HTTP到HTTPS重定向"
|
||||
echo "5. 运行: docker-compose -f docker-compose.prod.yml restart nginx"
|
||||
else
|
||||
echo "SSL证书申请失败,请检查:"
|
||||
echo "1. 域名DNS是否正确解析到本服务器"
|
||||
echo "2. 80端口是否可以从外网访问"
|
||||
echo "3. 邮箱地址是否正确"
|
||||
fi
|
||||
@@ -0,0 +1,124 @@
|
||||
# ============================================================================
|
||||
# OmniVerse Website - Kubernetes Deployment
|
||||
# Namespace: prod
|
||||
# Domain: www.omniverseai.net
|
||||
# ============================================================================
|
||||
|
||||
---
|
||||
# Deployment
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: website
|
||||
namespace: prod
|
||||
labels:
|
||||
app: website
|
||||
environment: production
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 1
|
||||
maxUnavailable: 0
|
||||
selector:
|
||||
matchLabels:
|
||||
app: website
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: website
|
||||
environment: production
|
||||
spec:
|
||||
containers:
|
||||
- name: website
|
||||
image: harbor.omniverseai.net/omniverse/website:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
imagePullSecrets:
|
||||
- name: harbor-secret
|
||||
|
||||
---
|
||||
# Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: website-svc
|
||||
namespace: prod
|
||||
labels:
|
||||
app: website
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: website
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
protocol: TCP
|
||||
|
||||
---
|
||||
# Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: website-ingress
|
||||
namespace: prod
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||
nginx.ingress.kubernetes.io/ssl-redirect: "false"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: www.omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: website-svc
|
||||
port:
|
||||
number: 80
|
||||
|
||||
- host: omniverseai.net
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: website-svc
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,30 @@
|
||||
# OmniVerse News Crawler Service Dockerfile
|
||||
FROM node:20-alpine
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --production
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Create data and log directories
|
||||
RUN mkdir -p /app/data /app/logs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3001
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
|
||||
|
||||
# Start application
|
||||
CMD ["npm", "start"]
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
|
||||
# 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;
|
||||
|
||||
# SPA fallback
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets
|
||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "OK\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# OmniVerse官网 - HTTP配置(用于SSL证书申请和重定向)
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.omniverseai.net omniverseai.net;
|
||||
|
||||
# Let's Encrypt验证目录
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# 其他请求重定向到HTTPS(证书配置后启用)
|
||||
location / {
|
||||
# 临时允许HTTP访问,用于测试
|
||||
proxy_pass http://web:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# 支持 Cloudflare HTTPS 代理
|
||||
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
# News API代理
|
||||
location /api/news {
|
||||
proxy_pass http://news-crawler:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS配置(获得证书后启用)
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name www.omniverseai.net omniverseai.net;
|
||||
#
|
||||
# # SSL证书配置
|
||||
# ssl_certificate /etc/letsencrypt/live/www.omniverseai.net/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/www.omniverseai.net/privkey.pem;
|
||||
# ssl_trusted_certificate /etc/letsencrypt/live/www.omniverseai.net/chain.pem;
|
||||
#
|
||||
# # SSL安全配置
|
||||
# ssl_protocols TLSv1.2 TLSv1.3;
|
||||
# ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
|
||||
# ssl_prefer_server_ciphers off;
|
||||
# ssl_session_cache shared:SSL:10m;
|
||||
# ssl_session_timeout 10m;
|
||||
# ssl_stapling on;
|
||||
# ssl_stapling_verify on;
|
||||
#
|
||||
# # 安全头
|
||||
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
# add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
# add_header X-Content-Type-Options "nosniff" always;
|
||||
# add_header X-XSS-Protection "1; mode=block" always;
|
||||
#
|
||||
# # 前端应用
|
||||
# location / {
|
||||
# proxy_pass http://web:3000;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection 'upgrade';
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_cache_bypass $http_upgrade;
|
||||
# }
|
||||
#
|
||||
# # News API代理
|
||||
# location /api/news {
|
||||
# proxy_pass http://news-crawler:3001;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# }
|
||||
# }
|
||||
@@ -0,0 +1,40 @@
|
||||
# Nginx配置 - OmniVerse官网生产环境
|
||||
|
||||
# 主配置
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 20M;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/x-javascript application/xml+rss
|
||||
application/javascript application/json;
|
||||
|
||||
# 包含站点配置
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
@@ -181,31 +181,30 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
: 'bg-transparent'
|
||||
}`}
|
||||
>
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
<div className="container mx-auto px-4 lg:px-6">
|
||||
<div className="flex items-center justify-between h-20 lg:h-24">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="block">
|
||||
<motion.div
|
||||
whileHover={{ scale: 1.05 }}
|
||||
className="flex items-center gap-3 cursor-pointer"
|
||||
className="flex items-center gap-3 lg:gap-4 cursor-pointer"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-cyan-500 blur-xl opacity-50"></div>
|
||||
<div className="relative bg-gradient-to-br from-cyan-400 to-green-400 p-2 rounded-xl">
|
||||
<Cpu size={28} className="text-black" />
|
||||
<div className="relative bg-gradient-to-br from-cyan-400 to-green-400 p-2 lg:p-2.5 rounded-xl">
|
||||
<Cpu size={28} className="text-black lg:w-8 lg:h-8" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold bg-gradient-to-r from-cyan-400 to-green-400 bg-clip-text text-transparent">
|
||||
<h1 className="text-xl lg:text-2xl font-bold bg-gradient-to-r from-cyan-400 to-green-400 bg-clip-text text-transparent">
|
||||
{t('company.name')}
|
||||
</h1>
|
||||
<p className="text-xs text-gray-400">{t('company.tagline')}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden lg:flex items-center gap-1">
|
||||
<div className="hidden xl:flex items-center gap-1.5">
|
||||
{navigationItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
@@ -216,7 +215,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
{item.href.startsWith('/blog') || item.href.startsWith('/articles') || item.href.startsWith('/docs') || item.href.startsWith('/industry-news') ? (
|
||||
<Link
|
||||
to={item.href}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${
|
||||
className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${
|
||||
activeDropdown === item.id
|
||||
? 'text-cyan-400 bg-cyan-500/10'
|
||||
: 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50'
|
||||
@@ -237,7 +236,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
) : item.href.startsWith('/about') || item.href.startsWith('/team') || item.href.startsWith('/history') || item.href.startsWith('/careers') || item.href.startsWith('/news') || item.href.startsWith('/investors') || item.href.startsWith('/csr') || item.href.startsWith('/contact') ? (
|
||||
<Link
|
||||
to={item.href}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${
|
||||
className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${
|
||||
activeDropdown === item.id
|
||||
? 'text-cyan-400 bg-cyan-500/10'
|
||||
: 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50'
|
||||
@@ -260,7 +259,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
href={item.href}
|
||||
onClick={(e) => handleSmoothScroll(e, item.id)}
|
||||
whileHover={{ y: -2 }}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-300 ${
|
||||
className={`flex items-center gap-2 px-3 xl:px-4 py-2.5 rounded-lg text-sm font-medium whitespace-nowrap transition-all duration-300 ${
|
||||
activeDropdown === item.id
|
||||
? 'text-cyan-400 bg-cyan-500/10'
|
||||
: 'text-gray-300 hover:text-cyan-400 hover:bg-gray-800/50'
|
||||
@@ -369,7 +368,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
</div>
|
||||
|
||||
{/* Right Side Actions */}
|
||||
<div className="hidden lg:flex items-center gap-4">
|
||||
<div className="hidden xl:flex items-center gap-4 lg:gap-6">
|
||||
{/* Search Button */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
@@ -418,28 +417,18 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
{isAuthenticated ? (
|
||||
<UserMenu />
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => onOpenAuthModal('login')}
|
||||
className={`px-4 py-2 border rounded-lg transition-all duration-300 ${
|
||||
effectiveTheme === 'dark'
|
||||
? 'text-cyan-400 border-cyan-500/50 hover:bg-cyan-500/10'
|
||||
: 'text-cyan-600 border-cyan-500/50 hover:bg-cyan-50'
|
||||
}`}
|
||||
>
|
||||
{t('nav.login')}
|
||||
</motion.button>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => onOpenAuthModal('register')}
|
||||
className="px-4 py-2 bg-gradient-to-r from-cyan-500 to-green-500 text-black font-bold rounded-lg hover:shadow-lg hover:shadow-cyan-500/30 transition-all duration-300"
|
||||
>
|
||||
{t('nav.getStarted')}
|
||||
</motion.button>
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => onOpenAuthModal('login')}
|
||||
className={`px-5 py-2.5 border rounded-lg text-sm font-medium transition-all duration-300 ${
|
||||
effectiveTheme === 'dark'
|
||||
? 'text-cyan-400 border-cyan-500/50 hover:bg-cyan-500/10'
|
||||
: 'text-cyan-600 border-cyan-500/50 hover:bg-cyan-50'
|
||||
}`}
|
||||
>
|
||||
{t('nav.login')}
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -448,7 +437,7 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="lg:hidden p-2 text-gray-300 hover:text-cyan-400 transition-colors"
|
||||
className="xl:hidden p-2 text-gray-300 hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</motion.button>
|
||||
@@ -465,9 +454,9 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: '100%' }}
|
||||
transition={{ type: 'tween', duration: 0.3 }}
|
||||
className="fixed inset-0 z-40 bg-gray-900/95 backdrop-blur-xl lg:hidden"
|
||||
className="fixed inset-0 z-40 bg-gray-900/95 backdrop-blur-xl xl:hidden"
|
||||
>
|
||||
<div className="flex flex-col h-full pt-20 pb-6 px-4 overflow-y-auto">
|
||||
<div className="flex flex-col h-full pt-24 lg:pt-28 pb-6 px-4 overflow-y-auto">
|
||||
{/* Search Bar */}
|
||||
<div className="mb-6">
|
||||
<motion.button
|
||||
@@ -612,8 +601,8 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 mb-6">
|
||||
<button
|
||||
<div className="mb-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
onOpenAuthModal('login');
|
||||
setIsMobileMenuOpen(false);
|
||||
@@ -622,15 +611,6 @@ const Navigation: React.FC<NavigationProps> = ({ onOpenAuthModal }) => {
|
||||
>
|
||||
{t('nav.login')}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onOpenAuthModal('register');
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full px-4 py-3 bg-gradient-to-r from-cyan-500 to-green-500 text-black font-bold rounded-lg hover:shadow-lg hover:shadow-cyan-500/30 transition-all duration-300"
|
||||
>
|
||||
{t('nav.getStarted')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { SearchProvider } from './contexts/SearchContext'
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import { FavoritesProvider } from './contexts/FavoritesContext'
|
||||
|
||||
const basename = import.meta.env.PROD ? '/website' : '';
|
||||
const basename = '';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: process.env.NODE_ENV === 'production' ? '/website/' : '/',
|
||||
base: '/',
|
||||
server: {
|
||||
port: 5174,
|
||||
host: '0.0.0.0'
|
||||
|
||||
Reference in New Issue
Block a user