diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..43014f3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,69 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js +.next +out +dist + +# Environment files +.env +.env.local +.env.production.local +.env.development.local +.env.test.local + +# IDE +.vscode +.idea +*.swp +*.swo +*~ +.DS_Store + +# Testing +coverage +.nyc_output + +# Git +.git +.gitignore + +# Documentation +README.md +CLAUDE.md +AUTH_SETUP.md +FEATURES.md +docs + +# Docker +Dockerfile +docker-compose*.yml +.dockerignore + +# CI/CD +.github +.gitlab-ci.yml +.circleci + +# Development +.eslintrc.json +.prettierrc +jest.config.js +cypress +cypress.json + +# Logs +logs +*.log + +# OS files +Thumbs.db + +# Temporary files +tmp +temp +.tmp \ No newline at end of file diff --git a/.env.example b/.env.example index 20b1942..0122c55 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # Database -DATABASE_URL="postgresql://postgres:password@localhost:5432/notion_clone" -MONGODB_URI="mongodb://localhost:27017/notion_clone" +DATABASE_URL="postgresql://postgres:password@localhost:5432/snapdocs" +MONGODB_URI="mongodb://localhost:27017/snapdocs" # Redis REDIS_URL="redis://localhost:6379" @@ -13,7 +13,7 @@ NEXTAUTH_SECRET="your-secret-key-change-this-in-production" S3_ENDPOINT="http://localhost:9000" S3_ACCESS_KEY="minioadmin" S3_SECRET_KEY="minioadmin" -S3_BUCKET="notion-clone" +S3_BUCKET="snapdocs" S3_REGION="us-east-1" # Socket.io for real-time diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..1938adb --- /dev/null +++ b/.env.production.example @@ -0,0 +1,59 @@ +# Production Environment Variables +# Copy this file to .env.production and fill in your values + +# Database +DATABASE_URL="postgresql://postgres:SECURE_PASSWORD@postgres:5432/snapdocs" +MONGODB_URI="mongodb://snapdocs_user:SECURE_PASSWORD@mongodb:27017/snapdocs?authSource=snapdocs" + +# Redis +REDIS_URL="redis://:SECURE_PASSWORD@redis:6379" +REDIS_PASSWORD="SECURE_PASSWORD" + +# NextAuth +NEXTAUTH_URL="https://your-domain.com" +NEXTAUTH_SECRET="generate-with-openssl-rand-base64-32" + +# File Storage (MinIO or S3) +S3_ENDPOINT="https://s3.your-domain.com" +S3_ACCESS_KEY="your-access-key" +S3_SECRET_KEY="your-secret-key" +S3_BUCKET="snapdocs" +S3_REGION="us-east-1" + +# Socket.io for real-time +NEXT_PUBLIC_SOCKET_URL="https://your-domain.com" + +# App Configuration +NEXT_PUBLIC_APP_URL="https://your-domain.com" + +# Database Passwords (for docker-compose) +POSTGRES_PASSWORD="SECURE_PASSWORD" +MONGO_ROOT_PASSWORD="SECURE_ROOT_PASSWORD" +MONGO_PASSWORD="SECURE_PASSWORD" + +# OAuth Providers (optional) +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" +GITHUB_CLIENT_ID="" +GITHUB_CLIENT_SECRET="" + +# Email Service (optional) +SMTP_HOST="" +SMTP_PORT="" +SMTP_USER="" +SMTP_PASSWORD="" +SMTP_FROM="" + +# Analytics (optional) +NEXT_PUBLIC_GOOGLE_ANALYTICS="" +NEXT_PUBLIC_POSTHOG_KEY="" +NEXT_PUBLIC_POSTHOG_HOST="" + +# Sentry Error Tracking (optional) +SENTRY_DSN="" +NEXT_PUBLIC_SENTRY_DSN="" + +# Feature Flags +ENABLE_SIGNUP="true" +ENABLE_OAUTH="true" +MAINTENANCE_MODE="false" \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b2d6960 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,52 @@ +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 10 + reviewers: + - "nevil" + labels: + - "dependencies" + - "npm" + commit-message: + prefix: "chore" + include: "scope" + + # Enable version updates for Docker + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 5 + reviewers: + - "nevil" + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "chore" + include: "scope" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "04:00" + open-pull-requests-limit: 5 + reviewers: + - "nevil" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "chore" + include: "scope" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a106046 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,243 @@ +name: CI/CD Pipeline + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +env: + NODE_VERSION: '20' + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + # Code Quality Checks + quality: + name: Code Quality + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run linter + run: npm run lint + + - name: Run type check + run: npm run typecheck + + - name: Run format check + run: npm run format:check || true + + # Security Scanning + security: + name: Security Scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: 'trivy-results.sarif' + + # Unit Tests + test: + name: Unit Tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: testpassword + POSTGRES_DB: snapdocs_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + mongodb: + image: mongo:7.0 + env: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: testpassword + MONGO_INITDB_DATABASE: snapdocs_test + options: >- + --health-cmd "echo 'db.runCommand(\"ping\").ok' | mongosh localhost:27017/test --quiet" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 27017:27017 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Setup test environment + run: | + cp .env.example .env.test + echo "DATABASE_URL=postgresql://postgres:testpassword@localhost:5432/snapdocs_test" >> .env.test + echo "MONGODB_URI=mongodb://admin:testpassword@localhost:27017/snapdocs_test?authSource=admin" >> .env.test + echo "REDIS_URL=redis://localhost:6379" >> .env.test + + - name: Run Prisma migrations + run: npx prisma migrate deploy + env: + DATABASE_URL: postgresql://postgres:testpassword@localhost:5432/snapdocs_test + + - name: Run tests + run: npm test -- --passWithNoTests + env: + NODE_ENV: test + DATABASE_URL: postgresql://postgres:testpassword@localhost:5432/snapdocs_test + MONGODB_URI: mongodb://admin:testpassword@localhost:27017/snapdocs_test?authSource=admin + REDIS_URL: redis://localhost:6379 + + # Build Docker Image + build: + name: Build Docker Image + runs-on: ubuntu-latest + needs: [quality, security, test] + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') + + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BUILDKIT_CONTEXT_KEEP_GIT_DIR=true + + # Deploy to Staging + deploy-staging: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/develop' + environment: + name: staging + url: https://staging.snapdocs.app + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy to staging server + run: | + echo "Deploying to staging environment" + # Add your deployment script here + # Example: SSH to server and run docker-compose + # Or trigger webhook to deployment service + + # Deploy to Production + deploy-production: + name: Deploy to Production + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + environment: + name: production + url: https://snapdocs.app + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy to production server + run: | + echo "Deploying to production environment" + # Add your deployment script here + # Example: SSH to server and run docker-compose + # Or trigger webhook to deployment service + + # Notify on Success + notify-success: + name: Notify Success + runs-on: ubuntu-latest + needs: [deploy-staging, deploy-production] + if: always() && (needs.deploy-staging.result == 'success' || needs.deploy-production.result == 'success') + + steps: + - name: Send success notification + run: | + echo "Deployment successful!" + # Add notification logic here (Slack, Discord, email, etc.) \ No newline at end of file diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..0fa65c6 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,88 @@ +name: Docker Build and Publish + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: 'Docker image tag' + required: true + default: 'latest' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + DOCKERHUB_IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/snapdocs + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + ${{ env.DOCKERHUB_IMAGE }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=${{ github.event.inputs.tag }},enable=${{ github.event_name == 'workflow_dispatch' }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BUILDKIT_CONTEXT_KEEP_GIT_DIR=true + + - name: Update Docker Hub Description + if: github.event_name == 'release' + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ secrets.DOCKERHUB_USERNAME }}/snapdocs + readme-filepath: ./README.md + short-description: 'SnapDocs - Modern collaborative document workspace' \ No newline at end of file diff --git a/AUTH_SETUP.md b/AUTH_SETUP.md index a6d4c7f..0297b86 100644 --- a/AUTH_SETUP.md +++ b/AUTH_SETUP.md @@ -49,7 +49,7 @@ Make sure to set these in your `.env.local`: ```bash # Database -DATABASE_URL="postgresql://postgres:password@localhost:5432/notion_clone" +DATABASE_URL="postgresql://postgres:password@localhost:5432/snapdocs" # NextAuth.js NEXTAUTH_SECRET="your-secret-key-here" @@ -110,4 +110,4 @@ NEXTAUTH_URL="http://localhost:3000" 3. Authenticated requests → Token validated → User session available 4. Protected routes → Check authentication → Redirect if needed -The system is ready for development and includes all necessary authentication features for a production-ready Notion clone. \ No newline at end of file +The system is ready for development and includes all necessary authentication features for a production-ready SnapDocs application. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 31ab18b..401580c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -This is a full-featured Notion clone built with: +SnapDocs is a modern collaborative document workspace built with: - **Next.js 14** (App Router) for the frontend and API - **PostgreSQL** for structured data (users, workspaces, page metadata) - **MongoDB** for document storage (page content blocks) @@ -116,7 +116,7 @@ See `prisma/schema.prisma` for the complete schema. Key models: - `User` - User accounts - `Workspace` - Team workspaces - `Page` - Page metadata -- `Database` - Notion databases +- `Database` - SnapDocs databases - `Permission` - Access control ### Environment Variables diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..58233a8 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,425 @@ +# SnapDocs Deployment Guide + +This guide covers various deployment options for SnapDocs, from simple one-click deployments to complex production setups. + +## Table of Contents +- [Quick Start](#quick-start) +- [Docker Deployment](#docker-deployment) +- [Cloud Deployments](#cloud-deployments) +- [Production Setup](#production-setup) +- [CI/CD Pipeline](#cicd-pipeline) +- [Environment Variables](#environment-variables) +- [SSL/TLS Configuration](#ssltls-configuration) +- [Monitoring & Maintenance](#monitoring--maintenance) + +## Quick Start + +### One-Click Deployment with Docker + +```bash +# Clone the repository +git clone https://github.com/yourusername/snapdocs.git +cd snapdocs + +# Copy environment variables +cp .env.production.example .env.production + +# Edit .env.production with your settings +nano .env.production + +# Start all services +docker-compose -f docker-compose.production.yml up -d + +# Run database migrations +docker-compose -f docker-compose.production.yml exec app npx prisma migrate deploy +``` + +Your application will be available at `http://localhost:3000` + +## Docker Deployment + +### Building the Docker Image + +```bash +# Build for production +docker build -t snapdocs:latest . + +# Or build with specific version tag +docker build -t snapdocs:v1.0.0 . + +# Multi-platform build (AMD64 and ARM64) +docker buildx build --platform linux/amd64,linux/arm64 -t snapdocs:latest . +``` + +### Running with Docker + +```bash +# Run standalone container +docker run -d \ + --name snapdocs \ + -p 3000:3000 \ + --env-file .env.production \ + snapdocs:latest + +# With docker-compose (recommended) +docker-compose -f docker-compose.production.yml up -d +``` + +### Docker Hub Deployment + +```bash +# Tag and push to Docker Hub +docker tag snapdocs:latest yourusername/snapdocs:latest +docker push yourusername/snapdocs:latest + +# Pull and run from Docker Hub +docker pull yourusername/snapdocs:latest +docker run -d -p 3000:3000 --env-file .env.production yourusername/snapdocs:latest +``` + +## Cloud Deployments + +### Deploy to Railway + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template) + +1. Click the button above +2. Configure environment variables +3. Deploy + +### Deploy to Render + +1. Create a new Web Service on Render +2. Connect your GitHub repository +3. Use the following settings: + - Build Command: `npm install && npm run build` + - Start Command: `npm start` +4. Add environment variables from `.env.production.example` + +### Deploy to DigitalOcean App Platform + +```bash +# Install doctl CLI +brew install doctl + +# Create app +doctl apps create --spec .do/app.yaml +``` + +### Deploy to AWS ECS + +```bash +# Build and push to ECR +aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $ECR_REGISTRY +docker build -t snapdocs . +docker tag snapdocs:latest $ECR_REGISTRY/snapdocs:latest +docker push $ECR_REGISTRY/snapdocs:latest + +# Deploy with ECS +aws ecs update-service --cluster snapdocs-cluster --service snapdocs-service --force-new-deployment +``` + +### Deploy to Google Cloud Run + +```bash +# Build and push to GCR +gcloud builds submit --tag gcr.io/PROJECT_ID/snapdocs + +# Deploy to Cloud Run +gcloud run deploy snapdocs \ + --image gcr.io/PROJECT_ID/snapdocs \ + --platform managed \ + --region us-central1 \ + --allow-unauthenticated +``` + +### Deploy to Azure Container Instances + +```bash +# Create resource group +az group create --name snapdocs-rg --location eastus + +# Create container instance +az container create \ + --resource-group snapdocs-rg \ + --name snapdocs \ + --image yourusername/snapdocs:latest \ + --dns-name-label snapdocs \ + --ports 3000 \ + --environment-variables-file .env.production +``` + +## Production Setup + +### Prerequisites + +- Docker and Docker Compose +- Domain name with DNS configured +- SSL certificates (or use Let's Encrypt) +- Minimum 2GB RAM, 2 CPU cores +- 20GB storage + +### Step-by-Step Production Deployment + +1. **Prepare the server** +```bash +# Update system +sudo apt update && sudo apt upgrade -y + +# Install Docker +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# Install Docker Compose +sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +sudo chmod +x /usr/local/bin/docker-compose +``` + +2. **Clone and configure** +```bash +# Clone repository +git clone https://github.com/yourusername/snapdocs.git /opt/snapdocs +cd /opt/snapdocs + +# Setup environment +cp .env.production.example .env.production +# Edit .env.production with your production values +``` + +3. **Setup SSL with Let's Encrypt** +```bash +# Install Certbot +sudo apt install certbot -y + +# Get certificates +sudo certbot certonly --standalone -d yourdomain.com -d www.yourdomain.com + +# Copy certificates to nginx directory +sudo cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem ./docker/nginx/ssl/ +sudo cp /etc/letsencrypt/live/yourdomain.com/privkey.pem ./docker/nginx/ssl/ +``` + +4. **Update nginx configuration** +```bash +# Edit nginx.conf +sed -i 's/your-domain.com/yourdomain.com/g' docker/nginx/nginx.conf +``` + +5. **Start services** +```bash +# Start all services +docker-compose -f docker-compose.production.yml up -d + +# Check logs +docker-compose -f docker-compose.production.yml logs -f + +# Run migrations +docker-compose -f docker-compose.production.yml exec app npx prisma migrate deploy + +# Create first admin user (optional) +docker-compose -f docker-compose.production.yml exec app npm run seed:admin +``` + +## CI/CD Pipeline + +### GitHub Actions Setup + +The repository includes GitHub Actions workflows for: +- Continuous Integration (testing, linting, type checking) +- Docker image building and publishing +- Automated deployment to staging/production + +To enable: + +1. **Set GitHub Secrets**: + - `DOCKERHUB_USERNAME`: Your Docker Hub username + - `DOCKERHUB_TOKEN`: Docker Hub access token + - `PRODUCTION_HOST`: Production server IP/hostname + - `PRODUCTION_SSH_KEY`: SSH private key for deployment + - `STAGING_HOST`: Staging server IP/hostname + - `STAGING_SSH_KEY`: SSH private key for staging + +2. **Configure deployment branches**: + - `main` branch → Production deployment + - `develop` branch → Staging deployment + +3. **Trigger deployment**: +```bash +# Deploy to staging +git push origin develop + +# Deploy to production +git push origin main + +# Or create a release +git tag v1.0.0 +git push origin v1.0.0 +``` + +## Environment Variables + +### Required Variables + +```env +# Database +DATABASE_URL=postgresql://user:pass@host:5432/db +MONGODB_URI=mongodb://user:pass@host:27017/db + +# Authentication +NEXTAUTH_URL=https://yourdomain.com +NEXTAUTH_SECRET=generate-with-openssl-rand-base64-32 + +# Redis +REDIS_URL=redis://host:6379 + +# File Storage +S3_ENDPOINT=https://s3.amazonaws.com +S3_ACCESS_KEY=your-key +S3_SECRET_KEY=your-secret +S3_BUCKET=your-bucket +``` + +### Optional Variables + +```env +# OAuth +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Email +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=your-email +SMTP_PASSWORD=your-password + +# Analytics +NEXT_PUBLIC_GOOGLE_ANALYTICS=GA-XXXXXX +SENTRY_DSN=https://xxx@sentry.io/xxx +``` + +## SSL/TLS Configuration + +### Using Let's Encrypt (Recommended) + +```bash +# Auto-renewal with cron +echo "0 0 * * * root certbot renew --quiet" | sudo tee -a /etc/crontab > /dev/null +``` + +### Using Custom SSL Certificates + +1. Place your certificates in `docker/nginx/ssl/` +2. Update `docker/nginx/nginx.conf` with correct paths +3. Restart nginx container + +## Monitoring & Maintenance + +### Health Checks + +```bash +# Check application health +curl http://localhost:3000/api/health + +# Check all services +docker-compose -f docker-compose.production.yml ps + +# View logs +docker-compose -f docker-compose.production.yml logs -f app +``` + +### Backup Strategy + +```bash +# Backup PostgreSQL +docker-compose -f docker-compose.production.yml exec postgres \ + pg_dump -U postgres snapdocs > backup-$(date +%Y%m%d).sql + +# Backup MongoDB +docker-compose -f docker-compose.production.yml exec mongodb \ + mongodump --db snapdocs --out /backup/ + +# Backup uploaded files (MinIO/S3) +aws s3 sync s3://snapdocs s3://snapdocs-backup-$(date +%Y%m%d)/ +``` + +### Update Deployment + +```bash +# Pull latest changes +git pull origin main + +# Rebuild and restart +docker-compose -f docker-compose.production.yml build +docker-compose -f docker-compose.production.yml up -d + +# Run migrations if needed +docker-compose -f docker-compose.production.yml exec app npx prisma migrate deploy +``` + +### Scaling + +For high-traffic deployments: + +1. **Horizontal Scaling**: +```yaml +# docker-compose.production.yml +app: + scale: 3 # Run 3 instances +``` + +2. **Load Balancing**: +- Use nginx as load balancer (included) +- Or use cloud load balancers (AWS ELB, GCP LB) + +3. **Database Scaling**: +- Use managed databases (RDS, Cloud SQL) +- Set up read replicas +- Implement connection pooling + +## Troubleshooting + +### Common Issues + +1. **Port already in use**: +```bash +# Find and kill process using port 3000 +sudo lsof -i :3000 +sudo kill -9 +``` + +2. **Database connection failed**: +```bash +# Check database is running +docker-compose -f docker-compose.production.yml ps postgres mongodb + +# Test connection +docker-compose -f docker-compose.production.yml exec app npx prisma db push +``` + +3. **Out of memory**: +```bash +# Increase Docker memory limit +docker update --memory="4g" snapdocs +``` + +4. **SSL certificate issues**: +```bash +# Renew certificates +sudo certbot renew --force-renewal + +# Restart nginx +docker-compose -f docker-compose.production.yml restart nginx +``` + +## Support + +For deployment issues: +- Check logs: `docker-compose logs -f` +- GitHub Issues: [github.com/yourusername/snapdocs/issues](https://github.com/yourusername/snapdocs/issues) +- Documentation: [docs.snapdocs.app](https://docs.snapdocs.app) + +## License + +SnapDocs is open source software licensed under the MIT license. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..58520bc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,73 @@ +# Multi-stage Docker build for Next.js application +# Stage 1: Dependencies +FROM node:20-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Copy package files +COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./ +COPY prisma ./prisma/ + +# Install dependencies based on the preferred package manager +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Generate Prisma Client +RUN npx prisma generate + +# Stage 2: Builder +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Set environment variables for build +ENV NEXT_TELEMETRY_DISABLED 1 +ENV NODE_ENV production + +# Build the application +RUN npm run build + +# Stage 3: Runner +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV production +ENV NEXT_TELEMETRY_DISABLED 1 + +# Create a non-root user +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy necessary files +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json + +# Set the correct permission for prerender cache +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Copy prisma schema and generate client +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma +COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma + +# Create necessary directories with correct permissions +RUN mkdir -p /app/.next/cache && chown -R nextjs:nodejs /app/.next + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 +ENV HOSTNAME "0.0.0.0" + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/api/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); });" + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/FEATURES.md b/FEATURES.md index af8d9f0..b72f7fd 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -1,4 +1,4 @@ -# Notion Clone - Feature Documentation +# SnapDocs - Feature Documentation ## 🚀 Implemented Features @@ -175,7 +175,7 @@ ## 🎨 UI/UX Features -- Clean, minimal interface inspired by Notion +- Clean, minimal interface for collaborative documentation - Dark mode support (system preference) - Responsive design for mobile/tablet - Smooth animations and transitions @@ -256,12 +256,12 @@ http://localhost:3000 Ensure `.env.local` has: ``` -DATABASE_URL=postgresql://user:password@localhost:5432/notion_clone -MONGODB_URI=mongodb://localhost:27017/notion_clone +DATABASE_URL=postgresql://user:password@localhost:5432/snapdocs +MONGODB_URI=mongodb://localhost:27017/snapdocs NEXTAUTH_SECRET=your-secret-key NEXTAUTH_URL=http://localhost:3000 ``` ## 🎉 Conclusion -This Notion clone implements all major features of Notion with a clean, performant architecture. The codebase is well-structured, type-safe, and ready for production deployment or further development. \ No newline at end of file +SnapDocs implements all major features of a modern collaborative workspace with a clean, performant architecture. The codebase is well-structured, type-safe, and ready for production deployment or further development. \ No newline at end of file diff --git a/README.md b/README.md index d6365f6..b0dd89c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Notion Clone +# SnapDocs -A full-featured Notion clone built with Next.js, PostgreSQL, MongoDB, and shadcn/ui. This application replicates core Notion functionality including rich text editing, nested pages, databases, real-time collaboration, and more. +SnapDocs is a modern collaborative document workspace built with Next.js, PostgreSQL, MongoDB, and shadcn/ui. It provides powerful features for creating, organizing, and collaborating on documents with your team. ## 🚀 Features @@ -53,7 +53,7 @@ A full-featured Notion clone built with Next.js, PostgreSQL, MongoDB, and shadcn ## 📁 Project Structure ``` -notion-clone/ +snapdocs/ ├── app/ # Next.js app directory │ ├── (auth)/ # Authentication pages │ ├── (main)/ # Main application @@ -92,8 +92,8 @@ notion-clone/ 1. **Clone the repository** ```bash -git clone https://github.com/yourusername/notion-clone.git -cd notion-clone +git clone https://github.com/yourusername/snapdocs.git +cd snapdocs ``` 2. **Install dependencies** @@ -114,8 +114,8 @@ cp .env.example .env.local Edit `.env.local` with your configuration: ```env # Database -DATABASE_URL="postgresql://postgres:password@localhost:5432/notion_clone" -MONGODB_URI="mongodb://localhost:27017/notion_clone" +DATABASE_URL="postgresql://postgres:password@localhost:5432/snapdocs" +MONGODB_URI="mongodb://localhost:27017/snapdocs" # Redis REDIS_URL="redis://localhost:6379" @@ -128,7 +128,7 @@ NEXTAUTH_SECRET="your-secret-key" S3_ENDPOINT="http://localhost:9000" S3_ACCESS_KEY="minioadmin" S3_SECRET_KEY="minioadmin" -S3_BUCKET="notion-clone" +S3_BUCKET="snapdocs" # Real-time SOCKET_URL="http://localhost:3000" @@ -252,8 +252,8 @@ npx shadcn-ui@latest add [component-name] ### Docker Production ```bash -docker build -t notion-clone . -docker run -p 3000:3000 notion-clone +docker build -t snapdocs . +docker run -p 3000:3000 snapdocs ``` ### Manual Deployment @@ -279,7 +279,7 @@ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) fi ## 🙏 Acknowledgments -- Inspired by [Notion](https://notion.so) +- Inspired by Notion's collaborative workspace concept - Built with [shadcn/ui](https://ui.shadcn.com) - Icons from [Lucide](https://lucide.dev) @@ -291,4 +291,4 @@ For issues and questions: --- -**Note**: This is an educational project and not affiliated with Notion Labs Inc. \ No newline at end of file +**Note**: This is an independent project and not affiliated with any other companies. \ No newline at end of file diff --git a/app/(protected)/ClientLayout.tsx b/app/(protected)/ClientLayout.tsx new file mode 100644 index 0000000..a9849fd --- /dev/null +++ b/app/(protected)/ClientLayout.tsx @@ -0,0 +1,11 @@ +'use client' + +import { SocketProvider } from '@/lib/socket/client' + +export function ClientLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 47005b1..8537767 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -28,7 +28,7 @@ export default async function DashboardPage() { return (
-

Welcome to Notion Clone

+

Welcome to SnapDocs

Create your first workspace from the sidebar dropdown to get started

diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx index 8fabec8..fc0fbe0 100644 --- a/app/(protected)/layout.tsx +++ b/app/(protected)/layout.tsx @@ -1,6 +1,7 @@ import { redirect } from "next/navigation" import { getCurrentUser } from "@/lib/auth" -import { NotionSidebar } from "@/components/layout/notion-sidebar" +import { SnapDocsSidebar } from "@/components/layout/snapdocs-sidebar" +import { ClientLayout } from "./ClientLayout" interface ProtectedLayoutProps { children: React.ReactNode @@ -14,11 +15,13 @@ export default async function ProtectedLayout({ children }: ProtectedLayoutProps } return ( -
- -
- {children} -
-
+ +
+ +
+ {children} +
+
+
) } \ No newline at end of file diff --git a/app/(protected)/workspace/[workspaceId]/WorkspaceDashboard.tsx b/app/(protected)/workspace/[workspaceId]/WorkspaceDashboard.tsx index a714d42..6edf913 100644 --- a/app/(protected)/workspace/[workspaceId]/WorkspaceDashboard.tsx +++ b/app/(protected)/workspace/[workspaceId]/WorkspaceDashboard.tsx @@ -1,298 +1,94 @@ 'use client' -import { useState } from 'react' -import { useRouter } from 'next/navigation' -import { Plus, Users, FileText, Archive, Clock, Sparkles } from 'lucide-react' -import { format } from 'date-fns' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { Avatar } from '@/components/ui/avatar' -import { Badge } from '@/components/ui/badge' -import { Separator } from '@/components/ui/separator' -import { NewPageDialog } from '@/components/pages/NewPageDialog' -import { Workspace, Page, User } from '@/types' +import { Clock } from 'lucide-react' +import Link from 'next/link' interface WorkspaceDashboardProps { - workspace: any // Extended workspace with members - recentPages: any[] // Extended pages with author - stats: { - totalPages: number - archivedPages: number - totalMembers: number - } - currentUser: User + workspace: any + recentPages: any[] + currentUser: any } export function WorkspaceDashboard({ workspace, recentPages, - stats, currentUser }: WorkspaceDashboardProps) { - const [newPageOpen, setNewPageOpen] = useState(false) - const router = useRouter() - - const handleCreatePage = () => { - setNewPageOpen(true) - } - - const handlePageClick = (pageId: string) => { - router.push(`/workspace/${workspace.id}/page/${pageId}`) - } const getGreeting = () => { const hour = new Date().getHours() - if (hour < 12) return 'Good morning' - if (hour < 17) return 'Good afternoon' - return 'Good evening' + const name = currentUser.name?.split(' ')[0] || 'there' + + if (hour < 12) return `Good morning, ${name}` + if (hour < 17) return `Good afternoon, ${name}` + return `Good evening, ${name}` + } + + const formatTime = (date: Date | string) => { + const d = new Date(date) + const now = new Date() + const diffMs = now.getTime() - d.getTime() + const diffMins = Math.floor(diffMs / 60000) + + if (diffMins < 1) return 'Just now' + if (diffMins < 60) return `${diffMins}m ago` + + const diffHours = Math.floor(diffMins / 60) + if (diffHours < 24) return `${diffHours}h ago` + + const diffDays = Math.floor(diffHours / 24) + if (diffDays < 7) return `${diffDays}d ago` + + return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) } return ( -
-
- {/* Header */} -
-
- {workspace.icon && ( - {workspace.icon} - )} -
-

{workspace.name}

-

- {getGreeting()}, {currentUser.name} -

+
+
+ {/* Simple Greeting */} +

+ {getGreeting()} +

+ + {/* Recent Pages - Simple List */} + {recentPages.length > 0 && ( +
+

+ + Recently visited +

+ +
+ {recentPages.slice(0, 10).map((page) => ( + + + {page.icon || '📄'} + + + {page.title || 'Untitled'} + + + {formatTime(page.updatedAt)} + + + ))}
- -
- - -
-
- - {/* Stats Cards */} -
- - - Total Pages - - - -
{stats.totalPages}
-

- {stats.archivedPages} archived -

-
-
- - - - Team Members - - - -
{stats.totalMembers}
-

- Active collaborators -

-
-
- - - - Recent Activity - - - -
{recentPages.length}
-

- Pages updated recently -

-
-
-
- -
- {/* Recent Pages */} -
- - - - - Recent Pages - - - Pages you've worked on recently - - - - {recentPages.length > 0 ? ( -
- {recentPages.map((page) => ( -
handlePageClick(page.id)} - > - {page.icon || '📄'} -
-
{page.title}
-
- Updated {format(new Date(page.updatedAt), 'MMM d, yyyy')} - {page.author?.name && ` by ${page.author.name}`} -
-
- {page.isPublished && ( - - Published - - )} -
- ))} -
- ) : ( -
- -
- No pages yet. Create your first page to get started. -
- -
- )} -
-
-
- - {/* Team Members */} -
- - - - - Team Members - - - People in this workspace - - - -
- {workspace.members.slice(0, 8).map((member: any) => ( -
- - {member.user.avatarUrl ? ( - {member.user.name - ) : ( -
- {(member.user.name || member.user.email).charAt(0).toUpperCase()} -
- )} -
-
-
- {member.user.name || member.user.email} -
-
- {member.role.toLowerCase()} -
-
-
- ))} - - {workspace.members.length > 8 && ( -
- +{workspace.members.length - 8} more members -
- )} -
-
-
+ )} + + {/* Empty State */} + {recentPages.length === 0 && ( +
+

+ No pages yet. Create a new page from the sidebar to get started. +

-
- - {/* Quick Actions */} -
- - - Quick Actions - - Common tasks and shortcuts - - - -
- - - - - -
-
-
-
+ )}
- - {/* New Page Dialog */} -
) } \ No newline at end of file diff --git a/app/(protected)/workspace/[workspaceId]/page.tsx b/app/(protected)/workspace/[workspaceId]/page.tsx index d52c251..a85ef9a 100644 --- a/app/(protected)/workspace/[workspaceId]/page.tsx +++ b/app/(protected)/workspace/[workspaceId]/page.tsx @@ -46,57 +46,37 @@ async function getWorkspaceData(workspaceId: string, userId: string) { return null } - // Get recent pages in workspace + // Get recent pages that the user has created or edited const recentPages = await prisma.page.findMany({ where: { workspaceId: workspace.id, isDeleted: false, - isArchived: false + isArchived: false, + OR: [ + { authorId: userId }, // Pages created by the user + { + updatedAt: { + gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // Pages updated in last 30 days + } + } + ] }, orderBy: { updatedAt: 'desc' }, - take: 10, - include: { - author: { - select: { - name: true, - avatarUrl: true - } - } + take: 15, // Show up to 15 recent pages + select: { + id: true, + title: true, + icon: true, + updatedAt: true, + authorId: true } }) - // Get workspace statistics - const [totalPages, archivedPages, totalMembers] = await Promise.all([ - prisma.page.count({ - where: { - workspaceId: workspace.id, - isDeleted: false - } - }), - prisma.page.count({ - where: { - workspaceId: workspace.id, - isArchived: true, - isDeleted: false - } - }), - prisma.workspaceMember.count({ - where: { - workspaceId: workspace.id - } - }) - ]) - return { workspace, - recentPages, - stats: { - totalPages, - archivedPages, - totalMembers - } + recentPages } } @@ -114,13 +94,12 @@ export default async function WorkspacePage({ params }: WorkspacePageProps) { notFound() } - const { workspace, recentPages, stats } = data + const { workspace, recentPages } = data return ( ) diff --git a/app/(protected)/workspace/[workspaceId]/page/[pageId]/PageEditor.tsx b/app/(protected)/workspace/[workspaceId]/page/[pageId]/PageEditor.tsx index 5d2fab8..c595e78 100644 --- a/app/(protected)/workspace/[workspaceId]/page/[pageId]/PageEditor.tsx +++ b/app/(protected)/workspace/[workspaceId]/page/[pageId]/PageEditor.tsx @@ -15,7 +15,7 @@ import { Menu } from 'lucide-react' import { Block, PageContent } from '@/types' -import NotionEditor from '@/components/editor/NotionEditor' +import SnapDocsEditor from '@/components/editor/SnapDocsEditor' import { Breadcrumbs } from '@/components/navigation/Breadcrumbs' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' @@ -289,7 +289,7 @@ export default function PageEditor({ page, initialContent, user }: PageEditorPro
{/* Editor */} - import('@/components/editor/SnapDocsEditor'), + { + ssr: false, + loading: () => ( +
+
+
+ ) + } +) interface PageData { id: string @@ -15,6 +29,7 @@ interface PageData { coverImage?: string isPublished: boolean isArchived: boolean + isFavorite?: boolean path: string workspaceId: string authorId: string @@ -50,17 +65,37 @@ interface PageEditorProps { export default function PageEditorV2({ page, initialContent, user }: PageEditorProps) { const router = useRouter() + const { isConnected, joinPage, leavePage } = useSocket() const [title, setTitle] = useState(page.title || '') const [icon, setIcon] = useState(page.icon || '') const [coverImage, setCoverImage] = useState(page.coverImage || '') const [showCoverOptions, setShowCoverOptions] = useState(false) const [showIconPicker, setShowIconPicker] = useState(false) const [isSaving, setIsSaving] = useState(false) + const [lastUpdated, setLastUpdated] = useState(page.updatedAt) const titleRef = useRef(null) const fileInputRef = useRef(null) const initialBlocks = initialContent?.blocks || [] + // Join the page room for real-time collaboration + useEffect(() => { + if (isConnected && user) { + joinPage(page.id, page.workspaceId, { + id: user.id, + name: user.name || 'Anonymous', + email: user.email || '', + avatarUrl: null + }) + } + + // Clean up when leaving the page + return () => { + leavePage() + } + }, [isConnected, page.id, page.workspaceId, user, joinPage, leavePage]) + + // Auto-resize title textarea useEffect(() => { if (titleRef.current) { @@ -75,7 +110,7 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP try { const response = await fetch(`/api/pages/${page.id}`, { - method: 'PUT', + method: 'PATCH', headers: { 'Content-Type': 'application/json', }, @@ -87,12 +122,18 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP if (!response.ok) { throw new Error('Failed to save title') } + + // Update the last updated timestamp + setLastUpdated(new Date().toISOString()) + + // Refresh the router to update the sidebar + router.refresh() } catch (error) { console.error('Error saving title:', error) toast.error('Failed to save title') setTitle(page.title || '') } - }, [page.id, page.title]) + }, [page.id, page.title, router]) // Debounced title save useEffect(() => { @@ -106,7 +147,7 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP }, [title, page.title, saveTitle]) // Auto-save page content - const handleAutoSave = useCallback(async (blocks: Block[]) => { + const handleAutoSave = useCallback(async (newBlocks: Block[]) => { setIsSaving(true) try { @@ -116,13 +157,18 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP 'Content-Type': 'application/json', }, body: JSON.stringify({ - blocks + blocks: newBlocks }), }) + const data = await response.json() + if (!response.ok) { - throw new Error('Failed to save content') + throw new Error(data.error || 'Failed to save content') } + + // Update the last updated timestamp + setLastUpdated(new Date().toISOString()) } catch (error) { console.error('Error saving content:', error) toast.error('Failed to save content') @@ -178,6 +224,9 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP if (!response.ok) { throw new Error('Failed to save page') } + + // Update the last updated timestamp + setLastUpdated(new Date().toISOString()) } catch (error) { console.error('Error saving page:', error) toast.error('Failed to save changes') @@ -234,49 +283,54 @@ export default function PageEditorV2({ page, initialContent, user }: PageEditorP const handleRefresh = () => { router.refresh() } + return (
- {/* Notion-style Page Header */} - - {/* Main Content */} -
- {/* Title */} -
-