fix: icons
49
.env.example
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ================================
|
||||||
|
# AWESOME APP - Environment Variables
|
||||||
|
# ================================
|
||||||
|
|
||||||
|
# Copy this file to .env and customize values
|
||||||
|
# Production: Copy to .env.production
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Compose Configuration
|
||||||
|
# ================================
|
||||||
|
AWESOME_COMPOSE_PROJECT_NAME=awesome
|
||||||
|
AWESOME_IMAGE=ghcr.io/valknarness/awesome-app:latest
|
||||||
|
AWESOME_PORT=3000
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Application Configuration
|
||||||
|
# ================================
|
||||||
|
NODE_ENV=production
|
||||||
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Database Configuration
|
||||||
|
# ================================
|
||||||
|
# Path to SQLite database file
|
||||||
|
AWESOME_DB_PATH=/app/awesome.db
|
||||||
|
|
||||||
|
# Volume path for database storage (production)
|
||||||
|
# AWESOME_DB_VOLUME=/var/lib/awesome/data
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Security & API
|
||||||
|
# ================================
|
||||||
|
# Webhook secret for database update notifications (generate random string)
|
||||||
|
AWESOME_WEBHOOK_SECRET=
|
||||||
|
|
||||||
|
# GitHub personal access token (optional, for higher rate limits)
|
||||||
|
# Get from: https://github.com/settings/tokens
|
||||||
|
AWESOME_GITHUB_TOKEN=
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# Traefik Configuration (Production Only)
|
||||||
|
# ================================
|
||||||
|
AWESOME_TRAEFIK_ENABLED=true
|
||||||
|
AWESOME_TRAEFIK_HOST=awesome.example.com
|
||||||
|
|
||||||
|
# ================================
|
||||||
|
# General Settings
|
||||||
|
# ================================
|
||||||
|
TIMEZONE=UTC
|
||||||
611
DOCKER.md
@@ -1,230 +1,447 @@
|
|||||||
# Docker Deployment Guide
|
# 🐳 Docker Deployment Guide
|
||||||
|
|
||||||
This guide covers building and deploying the awesome-app using Docker.
|
This guide covers deploying the **Awesome App** using Docker and Docker Compose.
|
||||||
|
|
||||||
## Quick Start
|
## 📋 Table of Contents
|
||||||
|
|
||||||
### Using Pre-built Image (Recommended)
|
- [Quick Start](#quick-start)
|
||||||
|
- [Compose Files](#compose-files)
|
||||||
|
- [Environment Variables](#environment-variables)
|
||||||
|
- [Production Deployment](#production-deployment)
|
||||||
|
- [Database Management](#database-management)
|
||||||
|
- [Traefik Integration](#traefik-integration)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
Pull and run the latest image from GitHub Container Registry:
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/valknarness/awesome-app:latest
|
# Copy environment example
|
||||||
docker run -p 3000:3000 ghcr.io/valknarness/awesome-app:latest
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit .env with your configuration
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker compose logs -f awesome-app
|
||||||
|
|
||||||
|
# Stop the application
|
||||||
|
docker compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
The image includes a pre-built database, updated every 6 hours by GitHub Actions.
|
The app will be available at `http://localhost:3000`
|
||||||
|
|
||||||
### Using Docker Compose
|
---
|
||||||
|
|
||||||
|
## 📁 Compose Files
|
||||||
|
|
||||||
|
### `compose.yml` (Base Configuration)
|
||||||
|
|
||||||
|
The base compose file for local development and testing:
|
||||||
|
- Uses the pre-built Docker image from GitHub Container Registry
|
||||||
|
- Exposes port 3000 for local access
|
||||||
|
- Mounts a local volume for database persistence
|
||||||
|
- Includes health checks
|
||||||
|
|
||||||
|
### `compose.production.yml` (Production Override)
|
||||||
|
|
||||||
|
Production configuration that extends the base:
|
||||||
|
- Integrates with Traefik reverse proxy
|
||||||
|
- Removes exposed ports (handled by Traefik)
|
||||||
|
- Adds compression middleware
|
||||||
|
- Configures HTTPS/TLS
|
||||||
|
- Uses external `compose_network`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Environment Variables
|
||||||
|
|
||||||
|
### Required Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Project name
|
||||||
|
AWESOME_COMPOSE_PROJECT_NAME=awesome
|
||||||
|
|
||||||
|
# Docker image
|
||||||
|
AWESOME_IMAGE=ghcr.io/valknarness/awesome-app:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional Variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Local port (development only)
|
||||||
|
AWESOME_PORT=3000
|
||||||
|
|
||||||
|
# Node environment
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# Database path inside container
|
||||||
|
AWESOME_DB_PATH=/app/awesome.db
|
||||||
|
|
||||||
|
# Database volume (production)
|
||||||
|
AWESOME_DB_VOLUME=/var/lib/awesome/data
|
||||||
|
|
||||||
|
# Webhook secret for updates
|
||||||
|
AWESOME_WEBHOOK_SECRET=your-secret-here
|
||||||
|
|
||||||
|
# GitHub token (for higher API rate limits)
|
||||||
|
AWESOME_GITHUB_TOKEN=ghp_your_token_here
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TIMEZONE=UTC
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traefik Variables (Production)
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Enable Traefik integration
|
||||||
|
AWESOME_TRAEFIK_ENABLED=true
|
||||||
|
|
||||||
|
# Your domain
|
||||||
|
AWESOME_TRAEFIK_HOST=awesome.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Production Deployment
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
1. **Docker & Docker Compose** installed
|
||||||
|
2. **Traefik** reverse proxy running (with `compose_network`)
|
||||||
|
3. **Domain** pointed to your server
|
||||||
|
4. **Environment variables** configured
|
||||||
|
|
||||||
|
### Step 1: Prepare Environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
# Create production environment file
|
||||||
|
cp .env.example .env.production
|
||||||
|
|
||||||
|
# Edit production settings
|
||||||
|
nano .env.production
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build Options
|
Required production settings:
|
||||||
|
```env
|
||||||
|
AWESOME_COMPOSE_PROJECT_NAME=awesome
|
||||||
|
AWESOME_IMAGE=ghcr.io/valknarness/awesome-app:latest
|
||||||
|
AWESOME_TRAEFIK_ENABLED=true
|
||||||
|
AWESOME_TRAEFIK_HOST=awesome.yourdomain.com
|
||||||
|
AWESOME_WEBHOOK_SECRET=generate-random-secret-here
|
||||||
|
AWESOME_DB_VOLUME=/var/lib/awesome/data
|
||||||
|
NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
The Dockerfile supports a build argument `INCLUDE_DATABASE` to control whether the database is embedded in the image or mounted at runtime.
|
### Step 2: Create Data Directory
|
||||||
|
|
||||||
### Option 1: Embedded Database (CI Default)
|
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Self-contained image
|
|
||||||
- No external dependencies
|
|
||||||
- Faster startup
|
|
||||||
- Database version matches image version
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Larger image size
|
|
||||||
- Database updates require new image build
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build --build-arg INCLUDE_DATABASE=true -t awesome-app .
|
# Create directory for database
|
||||||
docker run -p 3000:3000 awesome-app
|
sudo mkdir -p /var/lib/awesome/data
|
||||||
|
sudo chown -R 1001:1001 /var/lib/awesome/data
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 2: Volume-Mounted Database (Local Default)
|
### Step 3: Deploy
|
||||||
|
|
||||||
**Pros:**
|
|
||||||
- Smaller image size
|
|
||||||
- Database can be updated independently
|
|
||||||
- Easier for development
|
|
||||||
|
|
||||||
**Cons:**
|
|
||||||
- Requires database setup/volume mount
|
|
||||||
- Extra configuration needed
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build --build-arg INCLUDE_DATABASE=false -t awesome-app .
|
# Pull latest image
|
||||||
docker run -p 3000:3000 -v $(pwd)/data:/app/data awesome-app
|
docker compose -f compose.production.yml pull
|
||||||
|
|
||||||
|
# Start services
|
||||||
|
docker compose -f compose.production.yml up -d
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker compose -f compose.production.yml logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker Compose Configuration
|
### Step 4: Verify Deployment
|
||||||
|
|
||||||
Edit `docker-compose.yml` to control database inclusion:
|
```bash
|
||||||
|
# Check container status
|
||||||
|
docker compose -f compose.production.yml ps
|
||||||
|
|
||||||
|
# Check health
|
||||||
|
curl https://awesome.yourdomain.com/api/stats
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💾 Database Management
|
||||||
|
|
||||||
|
### Using Pre-built Database
|
||||||
|
|
||||||
|
The easiest way is to use a pre-built database from GitHub Actions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download database using GitHub CLI
|
||||||
|
gh run download --repo valknarness/awesome-app -n awesome-database
|
||||||
|
|
||||||
|
# Extract and place in data directory
|
||||||
|
sudo cp awesome.db /var/lib/awesome/data/
|
||||||
|
sudo chown 1001:1001 /var/lib/awesome/data/awesome.db
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mounting External Database
|
||||||
|
|
||||||
|
You can mount a pre-existing database:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In compose.yml or compose.production.yml
|
||||||
|
volumes:
|
||||||
|
- /path/to/your/awesome.db:/app/awesome.db:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Updates
|
||||||
|
|
||||||
|
The app can receive webhook notifications for database updates:
|
||||||
|
|
||||||
|
1. Set `AWESOME_WEBHOOK_SECRET` in environment
|
||||||
|
2. Configure GitHub Actions webhook to POST to `https://your-domain.com/api/webhook`
|
||||||
|
3. The app will invalidate cache and notify clients
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Traefik Integration
|
||||||
|
|
||||||
|
### Network Setup
|
||||||
|
|
||||||
|
Ensure Traefik's `compose_network` exists:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create compose_network
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traefik Configuration
|
||||||
|
|
||||||
|
The production compose file includes labels for:
|
||||||
|
- **HTTP to HTTPS redirect**
|
||||||
|
- **TLS/SSL certificates** (via Let's Encrypt)
|
||||||
|
- **Compression** middleware
|
||||||
|
- **Load balancing** configuration
|
||||||
|
|
||||||
|
Example Traefik labels:
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- 'traefik.enable=true'
|
||||||
|
- 'traefik.http.routers.awesome-web-secure.rule=Host(`awesome.example.com`)'
|
||||||
|
- 'traefik.http.routers.awesome-web-secure.tls.certresolver=resolver'
|
||||||
|
- 'traefik.http.routers.awesome-web-secure.entrypoints=web-secure'
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSL Certificates
|
||||||
|
|
||||||
|
Traefik automatically handles SSL certificates using Let's Encrypt when properly configured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Troubleshooting
|
||||||
|
|
||||||
|
### Container Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker compose logs awesome-app
|
||||||
|
|
||||||
|
# Check container status
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Restart container
|
||||||
|
docker compose restart awesome-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Not Found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if database exists
|
||||||
|
docker compose exec awesome-app ls -la /app/
|
||||||
|
|
||||||
|
# Check volume mounts
|
||||||
|
docker compose exec awesome-app df -h
|
||||||
|
|
||||||
|
# Verify permissions
|
||||||
|
docker compose exec awesome-app ls -la /app/data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Traefik Not Routing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check Traefik logs
|
||||||
|
docker logs traefik
|
||||||
|
|
||||||
|
# Verify network
|
||||||
|
docker network inspect compose_network
|
||||||
|
|
||||||
|
# Check labels
|
||||||
|
docker inspect awesome_app | grep traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Issues
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check resource usage
|
||||||
|
docker stats awesome_app
|
||||||
|
|
||||||
|
# Check database size
|
||||||
|
docker compose exec awesome-app du -h /app/awesome.db
|
||||||
|
|
||||||
|
# Restart with fresh container
|
||||||
|
docker compose down
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port Already in Use
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Change port in .env
|
||||||
|
AWESOME_PORT=3001
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Updates & Maintenance
|
||||||
|
|
||||||
|
### Update to Latest Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pull latest image
|
||||||
|
docker compose pull
|
||||||
|
|
||||||
|
# Recreate container
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Or for production
|
||||||
|
docker compose -f compose.production.yml pull
|
||||||
|
docker compose -f compose.production.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy database from container
|
||||||
|
docker compose cp awesome-app:/app/awesome.db ./backup-awesome.db
|
||||||
|
|
||||||
|
# Or from volume
|
||||||
|
sudo cp /var/lib/awesome/data/awesome.db ~/backup-awesome-$(date +%Y%m%d).db
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Follow logs
|
||||||
|
docker compose logs -f awesome-app
|
||||||
|
|
||||||
|
# Last 100 lines
|
||||||
|
docker compose logs --tail=100 awesome-app
|
||||||
|
|
||||||
|
# Since specific time
|
||||||
|
docker compose logs --since 1h awesome-app
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Health Checks
|
||||||
|
|
||||||
|
The container includes health checks that ping `/api/stats`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/stats"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
```
|
||||||
|
|
||||||
|
Check health status:
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
# Should show "healthy" status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Best Practices
|
||||||
|
|
||||||
|
1. **Always use .env files** for configuration (never commit secrets)
|
||||||
|
2. **Use named volumes** for data persistence
|
||||||
|
3. **Monitor logs** regularly for errors
|
||||||
|
4. **Backup database** before major updates
|
||||||
|
5. **Use health checks** to ensure availability
|
||||||
|
6. **Keep images updated** for security patches
|
||||||
|
7. **Use Traefik** for SSL/TLS in production
|
||||||
|
8. **Set proper timezone** for accurate timestamps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Advanced Configuration
|
||||||
|
|
||||||
|
### Custom Build
|
||||||
|
|
||||||
|
Build from source instead of using pre-built image:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# In compose.yml
|
||||||
|
services:
|
||||||
|
awesome-app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
INCLUDE_DATABASE: false
|
||||||
|
NODE_ENV: production
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Instances
|
||||||
|
|
||||||
|
Run multiple instances with different databases:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instance 1
|
||||||
|
AWESOME_COMPOSE_PROJECT_NAME=awesome1 \
|
||||||
|
AWESOME_PORT=3001 \
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Instance 2
|
||||||
|
AWESOME_COMPOSE_PROJECT_NAME=awesome2 \
|
||||||
|
AWESOME_PORT=3002 \
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Limits
|
||||||
|
|
||||||
|
Add resource constraints:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
awesome-app:
|
awesome-app:
|
||||||
build:
|
deploy:
|
||||||
args:
|
resources:
|
||||||
INCLUDE_DATABASE: false # Change to true to embed database
|
limits:
|
||||||
volumes:
|
cpus: '2'
|
||||||
- ./data:/app/data # Only needed when INCLUDE_DATABASE=false
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 1G
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
---
|
||||||
|
|
||||||
| Variable | Default | Description |
|
## 📚 Additional Resources
|
||||||
|----------|---------|-------------|
|
|
||||||
| `NODE_ENV` | `production` | Node.js environment |
|
|
||||||
| `PORT` | `3000` | Application port |
|
|
||||||
| `HOSTNAME` | `0.0.0.0` | Bind hostname |
|
|
||||||
|
|
||||||
## Database Location
|
- [Docker Documentation](https://docs.docker.com/)
|
||||||
|
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
||||||
|
- [Traefik Documentation](https://doc.traefik.io/traefik/)
|
||||||
|
- [GitHub Container Registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
|
||||||
|
|
||||||
- **Embedded mode**: `/app/awesome.db`
|
---
|
||||||
- **Volume mode**: `/app/data/awesome.db` (mounted)
|
|
||||||
|
|
||||||
The application will automatically detect and use the database from either location.
|
**Built with 💜💗💛 and maximum awesomeness!**
|
||||||
|
|
||||||
## Multi-Platform Support
|
|
||||||
|
|
||||||
Images are built for multiple platforms:
|
|
||||||
- `linux/amd64` (x86_64)
|
|
||||||
- `linux/arm64` (ARM64/Apple Silicon)
|
|
||||||
|
|
||||||
Docker will automatically pull the correct architecture for your system.
|
|
||||||
|
|
||||||
## Health Checks
|
|
||||||
|
|
||||||
The image includes a built-in health check that pings the application every 30 seconds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker ps # Check HEALTH status column
|
|
||||||
```
|
|
||||||
|
|
||||||
## Image Metadata
|
|
||||||
|
|
||||||
View database metadata embedded in the image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker inspect ghcr.io/valknarness/awesome-app:latest | jq '.[0].Config.Labels'
|
|
||||||
```
|
|
||||||
|
|
||||||
Metadata includes:
|
|
||||||
- `app.database.timestamp` - When the database was built
|
|
||||||
- `app.database.hash` - SHA256 hash of the database
|
|
||||||
- `app.database.lists_count` - Number of awesome lists
|
|
||||||
- `app.database.repos_count` - Number of repositories
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
### Using Pre-built Image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/valknarness/awesome-app:latest
|
|
||||||
docker run -d \
|
|
||||||
--name awesome-app \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/valknarness/awesome-app:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### With Volume Mount
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name awesome-app \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-v awesome-data:/app/data \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/valknarness/awesome-app:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using Docker Compose
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Updates
|
|
||||||
|
|
||||||
### Embedded Database
|
|
||||||
|
|
||||||
Pull the latest image to get an updated database:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/valknarness/awesome-app:latest
|
|
||||||
docker-compose up -d # Recreates container with new image
|
|
||||||
```
|
|
||||||
|
|
||||||
### Volume-Mounted Database
|
|
||||||
|
|
||||||
Update the database file in the mounted volume:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Download latest database
|
|
||||||
wget https://github.com/your-repo/releases/latest/download/awesome.db
|
|
||||||
|
|
||||||
# Place in volume
|
|
||||||
cp awesome.db ./data/
|
|
||||||
|
|
||||||
# Restart container
|
|
||||||
docker-compose restart
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Database not found
|
|
||||||
|
|
||||||
If the application can't find the database:
|
|
||||||
|
|
||||||
1. **Embedded mode**: Ensure `INCLUDE_DATABASE=true` was set during build
|
|
||||||
2. **Volume mode**: Check that the volume is mounted correctly
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec awesome-app ls -la /app/awesome.db # Embedded
|
|
||||||
docker exec awesome-app ls -la /app/data/awesome.db # Volume
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission issues
|
|
||||||
|
|
||||||
Ensure the database file has correct permissions:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec awesome-app chown nextjs:nodejs /app/data/awesome.db
|
|
||||||
```
|
|
||||||
|
|
||||||
### Rebuild from scratch
|
|
||||||
|
|
||||||
Remove cached layers and rebuild:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build --no-cache --build-arg INCLUDE_DATABASE=true -t awesome-app .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
For local development with hot reload:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use the dev server instead of Docker
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
For testing the production Docker build locally:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t awesome-app-test .
|
|
||||||
docker run -p 3000:3000 awesome-app-test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
The container runs as a non-root user (`nextjs:nodejs`) with UID/GID 1001 for enhanced security.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues or questions:
|
|
||||||
- GitHub Issues: [your-repo/issues](https://github.com/your-repo/issues)
|
|
||||||
- Workflow Docs: [.github/workflows/README.md](.github/workflows/README.md)
|
|
||||||
|
|||||||
217
PRODUCTION_SETUP.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# 🚀 Production Setup Guide - Quick Fix
|
||||||
|
|
||||||
|
## Current Issue
|
||||||
|
|
||||||
|
The app is running at `https://awesome.pivoine.art` but showing errors because **the database is missing**.
|
||||||
|
|
||||||
|
Errors you're seeing:
|
||||||
|
- `api/db-version: 404` - Database version endpoint can't find database
|
||||||
|
- `api/stats: 500` - Stats endpoint fails without database
|
||||||
|
- `api/lists: 500` - Lists endpoint fails without database
|
||||||
|
|
||||||
|
## Quick Fix (5 Minutes)
|
||||||
|
|
||||||
|
### Option 1: Automated Script (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On your production server
|
||||||
|
cd /opt/awesome # or wherever your compose files are
|
||||||
|
|
||||||
|
# Download and run the setup script
|
||||||
|
curl -O https://raw.githubusercontent.com/valknarness/awesome-app/main/scripts/setup-production-db.sh
|
||||||
|
chmod +x setup-production-db.sh
|
||||||
|
sudo ./setup-production-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script will:
|
||||||
|
1. ✅ Create the data directory
|
||||||
|
2. ✅ Download the latest database from GitHub Actions
|
||||||
|
3. ✅ Install it in the correct location
|
||||||
|
4. ✅ Set proper permissions (1001:1001 for nextjs user)
|
||||||
|
5. ✅ Restart the container
|
||||||
|
|
||||||
|
### Option 2: Manual Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Create data directory
|
||||||
|
sudo mkdir -p /var/lib/awesome/data
|
||||||
|
|
||||||
|
# 2. Download database using GitHub CLI
|
||||||
|
gh run list --repo valknarness/awesome-app --workflow "db.yml" --status success --limit 1
|
||||||
|
|
||||||
|
# Get the run ID from above, then:
|
||||||
|
gh run download <RUN_ID> --repo valknarness/awesome-app --name awesome-database
|
||||||
|
|
||||||
|
# 3. Install database
|
||||||
|
sudo cp awesome.db /var/lib/awesome/data/
|
||||||
|
sudo chown -R 1001:1001 /var/lib/awesome/data
|
||||||
|
|
||||||
|
# 4. Restart container
|
||||||
|
cd /opt/awesome
|
||||||
|
sudo docker compose -f compose.production.yml restart awesome-app
|
||||||
|
|
||||||
|
# 5. Verify
|
||||||
|
sudo docker compose -f compose.production.yml logs -f awesome-app
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Use the Database from awesome-app Build
|
||||||
|
|
||||||
|
If you don't have the database artifact, you need to build it first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trigger a database build
|
||||||
|
gh workflow run db.yml --repo valknarness/awesome-app
|
||||||
|
|
||||||
|
# Wait for it to complete (~5-10 minutes)
|
||||||
|
gh run watch
|
||||||
|
|
||||||
|
# Then follow Option 1 or 2 above
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verify Installation
|
||||||
|
|
||||||
|
After setup, check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Check database exists
|
||||||
|
sudo ls -lah /var/lib/awesome/data/
|
||||||
|
|
||||||
|
# Should show:
|
||||||
|
# awesome.db (50-200MB)
|
||||||
|
# db-metadata.json (optional)
|
||||||
|
|
||||||
|
# 2. Check container logs
|
||||||
|
sudo docker compose -f compose.production.yml logs awesome-app
|
||||||
|
|
||||||
|
# Should NOT show "Database file not found" errors
|
||||||
|
|
||||||
|
# 3. Test the API
|
||||||
|
curl https://awesome.pivoine.art/api/stats
|
||||||
|
|
||||||
|
# Should return JSON with stats, not 500 error
|
||||||
|
|
||||||
|
# 4. Visit the site
|
||||||
|
# https://awesome.pivoine.art
|
||||||
|
# Should show the homepage with real data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Container Configuration
|
||||||
|
|
||||||
|
Your production setup should have:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# compose.production.yml
|
||||||
|
services:
|
||||||
|
awesome-app:
|
||||||
|
volumes:
|
||||||
|
- /var/lib/awesome/data:/app/data
|
||||||
|
environment:
|
||||||
|
AWESOME_DB_PATH: /app/data/awesome.db # or /app/awesome.db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Path Options
|
||||||
|
|
||||||
|
The app checks for database in this order:
|
||||||
|
|
||||||
|
1. `AWESOME_DB_PATH` environment variable
|
||||||
|
2. `/app/awesome.db` (if database was built into image)
|
||||||
|
3. `/app/data/awesome.db` (if using volume mount)
|
||||||
|
4. `~/.awesome/awesome.db` (fallback)
|
||||||
|
|
||||||
|
For production with volume mount, use:
|
||||||
|
```env
|
||||||
|
AWESOME_DB_PATH=/app/data/awesome.db
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container can't find database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if database is mounted
|
||||||
|
sudo docker compose exec awesome-app ls -la /app/data/
|
||||||
|
|
||||||
|
# Check environment variable
|
||||||
|
sudo docker compose exec awesome-app env | grep AWESOME_DB_PATH
|
||||||
|
|
||||||
|
# Check volume mount
|
||||||
|
sudo docker compose config | grep -A 5 volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Fix permissions
|
||||||
|
sudo chown -R 1001:1001 /var/lib/awesome/data
|
||||||
|
sudo chmod -R 755 /var/lib/awesome/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container not restarting
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View logs
|
||||||
|
sudo docker compose -f compose.production.yml logs awesome-app
|
||||||
|
|
||||||
|
# Force recreate
|
||||||
|
sudo docker compose -f compose.production.yml up -d --force-recreate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative: Build Database Locally
|
||||||
|
|
||||||
|
If GitHub Actions database isn't available, build locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone awesome CLI
|
||||||
|
cd /tmp
|
||||||
|
git clone https://github.com/valknarness/awesome.git
|
||||||
|
cd awesome
|
||||||
|
|
||||||
|
# 2. Install dependencies
|
||||||
|
pnpm install
|
||||||
|
pnpm rebuild better-sqlite3
|
||||||
|
|
||||||
|
# 3. Build database (takes 1-2 hours!)
|
||||||
|
./awesome index
|
||||||
|
|
||||||
|
# 4. Copy to production location
|
||||||
|
sudo cp ~/.awesome/awesome.db /var/lib/awesome/data/
|
||||||
|
sudo chown 1001:1001 /var/lib/awesome/data/awesome.db
|
||||||
|
|
||||||
|
# 5. Restart container
|
||||||
|
cd /opt/awesome
|
||||||
|
sudo docker compose -f compose.production.yml restart awesome-app
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables Checklist
|
||||||
|
|
||||||
|
Make sure your `.env.production` has:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Required
|
||||||
|
AWESOME_COMPOSE_PROJECT_NAME=awesome
|
||||||
|
AWESOME_IMAGE=ghcr.io/valknarness/awesome-app:latest
|
||||||
|
AWESOME_DB_PATH=/app/data/awesome.db
|
||||||
|
AWESOME_DB_VOLUME=/var/lib/awesome/data
|
||||||
|
|
||||||
|
# Optional but recommended
|
||||||
|
AWESOME_WEBHOOK_SECRET=your-secret-here
|
||||||
|
AWESOME_GITHUB_TOKEN=ghp_your_token_here
|
||||||
|
|
||||||
|
# Traefik
|
||||||
|
AWESOME_TRAEFIK_ENABLED=true
|
||||||
|
AWESOME_TRAEFIK_HOST=awesome.pivoine.art
|
||||||
|
NETWORK_NAME=compose_network
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Once the database is installed and working:
|
||||||
|
|
||||||
|
1. **Set up automated updates**: Configure GitHub Actions webhook to notify the app when database updates
|
||||||
|
2. **Monitor logs**: `sudo docker compose logs -f awesome-app`
|
||||||
|
3. **Backup database**: Schedule regular backups of `/var/lib/awesome/data/awesome.db`
|
||||||
|
4. **Update regularly**: Pull new database builds every 6 hours
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need help?** Check the main [DOCKER.md](./DOCKER.md) guide for more details.
|
||||||
@@ -6,6 +6,19 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
outline-color: color-mix(in oklab, var(--ring) 50%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
border-color: var(--border);
|
||||||
|
outline-color: var(--ring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Awesome Gradient Text - Dynamic theme support */
|
/* Awesome Gradient Text - Dynamic theme support */
|
||||||
.gradient-text, .prose h1 {
|
.gradient-text, .prose h1 {
|
||||||
background: var(--gradient-awesome);
|
background: var(--gradient-awesome);
|
||||||
@@ -224,11 +237,6 @@ kbd {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus Ring */
|
|
||||||
*:focus-visible {
|
|
||||||
@apply outline-none ring-2 ring-primary ring-offset-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading Spinner */
|
/* Loading Spinner */
|
||||||
@keyframes spin-awesome {
|
@keyframes spin-awesome {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ export const metadata: Metadata = {
|
|||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||||
{ url: '/icon.svg', type: 'image/svg+xml', sizes: 'any' }
|
|
||||||
],
|
],
|
||||||
apple: '/apple-touch-icon.svg',
|
apple: '/apple-touch-icon.png',
|
||||||
shortcut: '/favicon.svg',
|
shortcut: '/favicon.svg',
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
CommandInput,
|
CommandInput,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from '@/components/ui/command'
|
} from '@/components/ui/command'
|
||||||
|
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Search, Star, BookOpen, Home, FileText, Code } from 'lucide-react'
|
import { Search, Star, BookOpen, Home, FileText, Code } from 'lucide-react'
|
||||||
|
|
||||||
interface CommandMenuProps {
|
interface CommandMenuProps {
|
||||||
@@ -18,20 +19,34 @@ interface CommandMenuProps {
|
|||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
repository_id: number
|
||||||
|
repository_name: string
|
||||||
|
repository_url: string
|
||||||
|
description: string | null
|
||||||
|
stars: number | null
|
||||||
|
language: string | null
|
||||||
|
topics: string | null
|
||||||
|
awesome_list_name: string | null
|
||||||
|
awesome_list_category: string | null
|
||||||
|
snippet: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResponse {
|
||||||
|
results: SearchResult[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
totalPages: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function CommandMenu({ open, setOpen }: CommandMenuProps) {
|
export function CommandMenu({ open, setOpen }: CommandMenuProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [search, setSearch] = React.useState('')
|
const [search, setSearch] = React.useState('')
|
||||||
const [results, setResults] = React.useState([])
|
const [results, setResults] = React.useState<SearchResult[]>([])
|
||||||
const [loading, setLoading] = React.useState(false)
|
const [loading, setLoading] = React.useState(false)
|
||||||
|
|
||||||
// declare the async data fetching function
|
|
||||||
const fetchData = React.useCallback(async () => {
|
|
||||||
const response = await fetch(`/api/search?q=${encodeURIComponent(search)}`)
|
|
||||||
const data = await response.json()
|
|
||||||
setResults(data.results);
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -45,13 +60,53 @@ const fetchData = React.useCallback(async () => {
|
|||||||
}, [open, setOpen])
|
}, [open, setOpen])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!search) {
|
// Clear results if search is empty
|
||||||
|
if (!search || search.trim() === '') {
|
||||||
|
setResults([])
|
||||||
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setLoading(true)
|
|
||||||
fetchData()
|
// Debounce search
|
||||||
console.log(results)
|
const timer = setTimeout(async () => {
|
||||||
setLoading(false)
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
// Match the search page API call with same parameters
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: search,
|
||||||
|
page: '1',
|
||||||
|
sortBy: 'relevance',
|
||||||
|
limit: '10' // Limit to 10 results for command menu
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(`/api/search?${params}`)
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Search API error:', response.status, response.statusText)
|
||||||
|
setResults([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: SearchResponse = await response.json()
|
||||||
|
|
||||||
|
// Check if response has error or invalid data
|
||||||
|
if (!data.results) {
|
||||||
|
console.error('Invalid search response:', data)
|
||||||
|
setResults([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Search results:', data.results.length, 'results for:', search)
|
||||||
|
setResults(data.results)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error)
|
||||||
|
setResults([])
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
|
||||||
|
return () => clearTimeout(timer)
|
||||||
}, [search])
|
}, [search])
|
||||||
|
|
||||||
const runCommand = React.useCallback((command: () => void) => {
|
const runCommand = React.useCallback((command: () => void) => {
|
||||||
@@ -94,13 +149,19 @@ const fetchData = React.useCallback(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput
|
<DialogContent className="overflow-hidden p-0" aria-describedby={undefined}>
|
||||||
placeholder="Search awesome lists, repos, and more..."
|
<DialogTitle className="sr-only">Search</DialogTitle>
|
||||||
value={search}
|
<Command
|
||||||
onValueChange={setSearch}
|
shouldFilter={false}
|
||||||
/>
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||||
<CommandList>
|
>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Search awesome lists, repos, and more..."
|
||||||
|
value={search}
|
||||||
|
onValueChange={setSearch}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center py-6">
|
<div className="flex items-center justify-center py-6">
|
||||||
@@ -133,17 +194,17 @@ const fetchData = React.useCallback(async () => {
|
|||||||
|
|
||||||
{results.length > 0 && (
|
{results.length > 0 && (
|
||||||
<CommandGroup heading="Search Results">
|
<CommandGroup heading="Search Results">
|
||||||
{results.map((result: any) => (
|
{results.map((result) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={result.repository_id}
|
key={result.repository_id}
|
||||||
value={result.repository_name}
|
value={result.repository_name}
|
||||||
onSelect={() => runCommand(() => router.push(result.url))}
|
onSelect={() => runCommand(() => router.push(`/repository/${result.repository_id}`))}
|
||||||
>
|
>
|
||||||
{getIcon(result.type)}
|
<Code className="mr-2 h-4 w-4" />
|
||||||
<div className="flex flex-1 flex-col gap-1">
|
<div className="flex flex-1 flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="font-medium">{result.title}</span>
|
<span className="font-medium">{result.repository_name}</span>
|
||||||
{result.stars && (
|
{result.stars !== null && (
|
||||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
<Star className="h-3 w-3 fill-current" />
|
<Star className="h-3 w-3 fill-current" />
|
||||||
{result.stars.toLocaleString()}
|
{result.stars.toLocaleString()}
|
||||||
@@ -155,17 +216,26 @@ const fetchData = React.useCallback(async () => {
|
|||||||
{result.description}
|
{result.description}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{result.category && (
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-xs text-primary">
|
{result.language && (
|
||||||
{result.category}
|
<span className="text-xs text-muted-foreground">
|
||||||
</span>
|
{result.language}
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
{result.awesome_list_category && (
|
||||||
|
<span className="text-xs text-primary">
|
||||||
|
{result.awesome_list_category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
)}
|
)}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
const badgeVariants = cva(
|
||||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-hidden focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Command as CommandPrimitive } from "cmdk"
|
|||||||
import { Search } from "lucide-react"
|
import { Search } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
const Command = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
@@ -26,7 +26,8 @@ Command.displayName = CommandPrimitive.displayName
|
|||||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0">
|
<DialogContent className="overflow-hidden p-0" aria-describedby={undefined}>
|
||||||
|
<DialogTitle className="sr-only">Command Menu</DialogTitle>
|
||||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
{children}
|
{children}
|
||||||
</Command>
|
</Command>
|
||||||
@@ -44,7 +45,7 @@ const CommandInput = React.forwardRef<
|
|||||||
<CommandPrimitive.Input
|
<CommandPrimitive.Input
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 focus:outline-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -115,7 +116,7 @@ const CommandItem = React.forwardRef<
|
|||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-none transition-[color,box-shadow]",
|
"group/input-group border-input dark:bg-input/30 shadow-xs relative flex w-full items-center rounded-md border outline-hidden transition-[color,box-shadow]",
|
||||||
"h-9 has-[>textarea]:h-auto",
|
"h-9 has-[>textarea]:h-auto",
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-hidden file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
|||||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||||
|
|
||||||
const navigationMenuTriggerStyle = cva(
|
const navigationMenuTriggerStyle = cva(
|
||||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
||||||
)
|
)
|
||||||
|
|
||||||
const NavigationMenuTrigger = React.forwardRef<
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
|||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ export const EditorProvider = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className={cn(className, '[&_.ProseMirror-focused]:outline-none')}>
|
<div className={cn(className, '[&_.ProseMirror-focused]:outline-hidden')}>
|
||||||
<TiptapEditorProvider
|
<TiptapEditorProvider
|
||||||
editorProps={{
|
editorProps={{
|
||||||
handleKeyDown: (_view, event) => {
|
handleKeyDown: (_view, event) => {
|
||||||
@@ -1374,7 +1374,7 @@ export const EditorLinkSelector = ({
|
|||||||
<form className="flex p-1" onSubmit={handleSubmit}>
|
<form className="flex p-1" onSubmit={handleSubmit}>
|
||||||
<input
|
<input
|
||||||
aria-label="Link URL"
|
aria-label="Link URL"
|
||||||
className="flex-1 bg-background p-1 text-sm outline-none"
|
className="flex-1 bg-background p-1 text-sm outline-hidden"
|
||||||
defaultValue={defaultValue ?? ''}
|
defaultValue={defaultValue ?? ''}
|
||||||
onChange={(event) => setUrl(event.target.value)}
|
onChange={(event) => setUrl(event.target.value)}
|
||||||
placeholder="Paste a link"
|
placeholder="Paste a link"
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ const SidebarGroupLabel = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="group-label"
|
data-sidebar="group-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
@@ -470,7 +470,7 @@ const SidebarGroupAction = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="group-action"
|
data-sidebar="group-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
// Increases the hit area of the button on mobile.
|
// Increases the hit area of the button on mobile.
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
"group-data-[collapsible=icon]:hidden",
|
"group-data-[collapsible=icon]:hidden",
|
||||||
@@ -522,7 +522,7 @@ const SidebarMenuItem = React.forwardRef<
|
|||||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||||
|
|
||||||
const sidebarMenuButtonVariants = cva(
|
const sidebarMenuButtonVariants = cva(
|
||||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -616,7 +616,7 @@ const SidebarMenuAction = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="menu-action"
|
data-sidebar="menu-action"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||||
// Increases the hit area of the button on mobile.
|
// Increases the hit area of the button on mobile.
|
||||||
"after:absolute after:-inset-2 after:md:hidden",
|
"after:absolute after:-inset-2 after:md:hidden",
|
||||||
"peer-data-[size=sm]/menu-button:top-1",
|
"peer-data-[size=sm]/menu-button:top-1",
|
||||||
@@ -732,7 +732,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
|||||||
data-size={size}
|
data-size={size}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||||
size === "sm" && "text-xs",
|
size === "sm" && "text-xs",
|
||||||
size === "md" && "text-sm",
|
size === "md" && "text-sm",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
|||||||
<textarea
|
<textarea
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-hidden focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
62
compose.production.yml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
include:
|
||||||
|
- compose.yml
|
||||||
|
|
||||||
|
# Production compose file - extends base compose.yml
|
||||||
|
# Usage: docker compose -f compose.production.yml up -d
|
||||||
|
|
||||||
|
networks:
|
||||||
|
compose_network:
|
||||||
|
external: true
|
||||||
|
name: compose_network
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Override Awesome App for production
|
||||||
|
awesome-app:
|
||||||
|
networks:
|
||||||
|
- compose_network
|
||||||
|
ports: [] # Remove exposed ports, use Traefik instead
|
||||||
|
|
||||||
|
# Override environment for production settings
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
|
|
||||||
|
# Database path (production)
|
||||||
|
AWESOME_DB_PATH: ${AWESOME_DB_PATH:-/app/awesome.db}
|
||||||
|
|
||||||
|
# Webhook secret (required for production updates)
|
||||||
|
WEBHOOK_SECRET: ${AWESOME_WEBHOOK_SECRET}
|
||||||
|
|
||||||
|
# GitHub token (for higher rate limits)
|
||||||
|
GITHUB_TOKEN: ${AWESOME_GITHUB_TOKEN:-}
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
PORT: 3000
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
|
||||||
|
# Override volume for production path
|
||||||
|
volumes:
|
||||||
|
- ${AWESOME_DB_VOLUME:-/var/lib/awesome/data}:/app/data
|
||||||
|
|
||||||
|
labels:
|
||||||
|
# Traefik labels for reverse proxy
|
||||||
|
- 'traefik.enable=${AWESOME_TRAEFIK_ENABLED:-true}'
|
||||||
|
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
|
- 'traefik.http.middlewares.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-redirect-web-secure.redirectscheme.scheme=https'
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web.middlewares=${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-redirect-web-secure'
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web.rule=Host(`${AWESOME_TRAEFIK_HOST}`)'
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web.entrypoints=web'
|
||||||
|
|
||||||
|
# HTTPS configuration
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web-secure.rule=Host(`${AWESOME_TRAEFIK_HOST}`)'
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web-secure.tls.certresolver=resolver'
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web-secure.entrypoints=web-secure'
|
||||||
|
|
||||||
|
# Compression middleware
|
||||||
|
- 'traefik.http.middlewares.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-compress.compress=true'
|
||||||
|
- 'traefik.http.routers.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web-secure.middlewares=${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-compress'
|
||||||
|
|
||||||
|
# Load balancer configuration
|
||||||
|
- 'traefik.http.services.${AWESOME_COMPOSE_PROJECT_NAME:-awesome}-web-secure.loadbalancer.server.port=3000'
|
||||||
|
- 'traefik.docker.network=${NETWORK_NAME}'
|
||||||
60
compose.yml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
services:
|
||||||
|
# Awesome App - Next.js application for exploring awesome lists
|
||||||
|
awesome-app:
|
||||||
|
image: ${AWESOME_IMAGE:-ghcr.io/valknarness/awesome-app:latest}
|
||||||
|
container_name: ${AWESOME_COMPOSE_PROJECT_NAME:-awesome}_app
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- awesome-network
|
||||||
|
ports:
|
||||||
|
- "${AWESOME_PORT:-3000}:3000"
|
||||||
|
environment:
|
||||||
|
# Node
|
||||||
|
NODE_ENV: ${NODE_ENV:-production}
|
||||||
|
PORT: 3000
|
||||||
|
HOSTNAME: 0.0.0.0
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-1}
|
||||||
|
|
||||||
|
# Database path
|
||||||
|
AWESOME_DB_PATH: ${AWESOME_DB_PATH:-/app/awesome.db}
|
||||||
|
|
||||||
|
# Optional: Webhook secret for database updates
|
||||||
|
WEBHOOK_SECRET: ${AWESOME_WEBHOOK_SECRET:-}
|
||||||
|
|
||||||
|
# Optional: GitHub token for rate limits
|
||||||
|
GITHUB_TOKEN: ${AWESOME_GITHUB_TOKEN:-}
|
||||||
|
|
||||||
|
# Timezone
|
||||||
|
TZ: ${TIMEZONE:-UTC}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Mount database directory for persistence
|
||||||
|
- ${AWESOME_DB_VOLUME:-awesome-data}:/app/data
|
||||||
|
# Optional: Mount a pre-existing database
|
||||||
|
# - ./awesome.db:/app/awesome.db:ro
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/stats"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
# Uncomment for development with local build
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: Dockerfile
|
||||||
|
# args:
|
||||||
|
# INCLUDE_DATABASE: false
|
||||||
|
# NODE_ENV: production
|
||||||
|
|
||||||
|
networks:
|
||||||
|
awesome-network:
|
||||||
|
driver: bridge
|
||||||
|
name: ${AWESOME_COMPOSE_PROJECT_NAME:-awesome}_network
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
awesome-data:
|
||||||
|
driver: local
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
services:
|
|
||||||
awesome-app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
# Set to true to include pre-built database in image
|
|
||||||
# Set to false to mount database at runtime
|
|
||||||
INCLUDE_DATABASE: false
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
volumes:
|
|
||||||
# Mount SQLite database directory (used when INCLUDE_DATABASE=false)
|
|
||||||
- ./data:/app/data
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -1,25 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="180" height="180" viewBox="0 0 180 180">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#DA22FF;stop-opacity:1" />
|
|
||||||
<stop offset="50%" style="stop-color:#9733EE;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Background with rounded corners for iOS -->
|
|
||||||
<rect width="180" height="180" rx="40" fill="url(#bg-gradient)" />
|
|
||||||
|
|
||||||
<!-- Awesome icon centered and scaled -->
|
|
||||||
<g transform="translate(90, 90) scale(2.8)">
|
|
||||||
<path fill="#FFFFFF" opacity="0.3" d="m14.8 8.625l-.3-1.5-4.95 1.05V2.5h-1.5v4.6125l-5.2125-3.375-.825 1.275 5.7 3.675-5.7 8.25 1.2.9 4.2375-5.55 3.15 4.7625 1.275-.825-3.15-4.6875z" />
|
|
||||||
<circle cx="9" cy="9" r="2.625" fill="#FFFFFF" />
|
|
||||||
<g fill="#FFFFFF" opacity="0.9">
|
|
||||||
<circle cx="9" cy="3" r="1.875" />
|
|
||||||
<circle cx="14.625" cy="7.875" r="1.875" />
|
|
||||||
<circle cx="2.625" cy="4.875" r="1.875" />
|
|
||||||
<circle cx="4.125" cy="15.375" r="1.875" />
|
|
||||||
<circle cx="12.75" cy="14.625" r="1.875" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 659 B After Width: | Height: | Size: 103 KiB |
@@ -1,27 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 48 48">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg-192" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#DA22FF;stop-opacity:1" />
|
|
||||||
<stop offset="50%" style="stop-color:#9733EE;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="48" height="48" fill="url(#bg-192)" />
|
|
||||||
|
|
||||||
<!-- Main structure - White with opacity -->
|
|
||||||
<path fill="#FFFFFF" opacity="0.3" d="m39.4 23l-.8-4L26 21.6V8h-4v12.3l-13.9-9l-2.2 3.4l15.2 9.8L9.4 39.8l3.2 2.4l11.3-14.8l8.4 12.7l3.4-2.2l-8.4-12.5z" />
|
|
||||||
|
|
||||||
<!-- Center circle - White -->
|
|
||||||
<circle cx="24" cy="24" r="7" fill="#FFFFFF" />
|
|
||||||
|
|
||||||
<!-- Outer circles - White with high opacity -->
|
|
||||||
<g fill="#FFFFFF" opacity="0.9">
|
|
||||||
<circle cx="24" cy="8" r="5" />
|
|
||||||
<circle cx="39" cy="21" r="5" />
|
|
||||||
<circle cx="7" cy="13" r="5" />
|
|
||||||
<circle cx="11" cy="41" r="5" />
|
|
||||||
<circle cx="34" cy="39" r="5" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,27 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 48 48">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="bg-512" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#DA22FF;stop-opacity:1" />
|
|
||||||
<stop offset="50%" style="stop-color:#9733EE;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="48" height="48" fill="url(#bg-512)" />
|
|
||||||
|
|
||||||
<!-- Main structure - White with opacity -->
|
|
||||||
<path fill="#FFFFFF" opacity="0.3" d="m39.4 23l-.8-4L26 21.6V8h-4v12.3l-13.9-9l-2.2 3.4l15.2 9.8L9.4 39.8l3.2 2.4l11.3-14.8l8.4 12.7l3.4-2.2l-8.4-12.5z" />
|
|
||||||
|
|
||||||
<!-- Center circle - White -->
|
|
||||||
<circle cx="24" cy="24" r="7" fill="#FFFFFF" />
|
|
||||||
|
|
||||||
<!-- Outer circles - White with high opacity -->
|
|
||||||
<g fill="#FFFFFF" opacity="0.9">
|
|
||||||
<circle cx="24" cy="8" r="5" />
|
|
||||||
<circle cx="39" cy="21" r="5" />
|
|
||||||
<circle cx="7" cy="13" r="5" />
|
|
||||||
<circle cx="11" cy="41" r="5" />
|
|
||||||
<circle cx="34" cy="39" r="5" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,15 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
||||||
<!-- Simplified awesome icon for small sizes -->
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="awesome-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#DA22FF;stop-opacity:1" />
|
|
||||||
<stop offset="50%" style="stop-color:#FF69B4;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Center star shape -->
|
|
||||||
<circle cx="16" cy="16" r="14" fill="url(#awesome-gradient)" />
|
|
||||||
<circle cx="16" cy="16" r="8" fill="#FF69B4" />
|
|
||||||
<circle cx="16" cy="16" r="4" fill="#DA22FF" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 708 B |
@@ -9,27 +9,15 @@
|
|||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icon.svg",
|
"src": "/web-app-manifest-192x192.png",
|
||||||
"sizes": "any",
|
|
||||||
"type": "image/svg+xml",
|
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/icon-192.svg",
|
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/svg+xml",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icon-512.svg",
|
"src": "/web-app-manifest-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/svg+xml",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "/apple-touch-icon.svg",
|
|
||||||
"sizes": "180x180",
|
|
||||||
"type": "image/svg+xml",
|
|
||||||
"purpose": "maskable"
|
"purpose": "maskable"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
BIN
public/og-image.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
@@ -1,73 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="og-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#DA22FF;stop-opacity:1" />
|
|
||||||
<stop offset="30%" style="stop-color:#9733EE;stop-opacity:1" />
|
|
||||||
<stop offset="70%" style="stop-color:#FF69B4;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<linearGradient id="text-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" style="stop-color:#DA22FF;stop-opacity:1" />
|
|
||||||
<stop offset="50%" style="stop-color:#9733EE;stop-opacity:1" />
|
|
||||||
<stop offset="100%" style="stop-color:#FFD700;stop-opacity:1" />
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<!-- Glow filter -->
|
|
||||||
<filter id="glow">
|
|
||||||
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
|
|
||||||
<feMerge>
|
|
||||||
<feMergeNode in="coloredBlur"/>
|
|
||||||
<feMergeNode in="SourceGraphic"/>
|
|
||||||
</feMerge>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Background -->
|
|
||||||
<rect width="1200" height="630" fill="url(#og-gradient)" />
|
|
||||||
|
|
||||||
<!-- Decorative circles -->
|
|
||||||
<circle cx="100" cy="100" r="150" fill="#FFFFFF" opacity="0.05" />
|
|
||||||
<circle cx="1100" cy="530" r="200" fill="#FFFFFF" opacity="0.05" />
|
|
||||||
<circle cx="900" cy="100" r="120" fill="#FFD700" opacity="0.1" />
|
|
||||||
<circle cx="300" cy="500" r="80" fill="#FF69B4" opacity="0.15" />
|
|
||||||
|
|
||||||
<!-- Main content container -->
|
|
||||||
<rect x="100" y="150" width="1000" height="330" rx="20" fill="#FFFFFF" opacity="0.95" />
|
|
||||||
|
|
||||||
<!-- Awesome Icon (centered at top) -->
|
|
||||||
<g transform="translate(500, 200) scale(3.5)" filter="url(#glow)">
|
|
||||||
<path fill="#9733EE" d="m14.8 8.625l-.3-1.5-4.95 1.05V2.5h-1.5v4.6125l-5.2125-3.375-.825 1.275 5.7 3.675-5.7 8.25 1.2.9 4.2375-5.55 3.15 4.7625 1.275-.825-3.15-4.6875z" />
|
|
||||||
<circle cx="9" cy="9" r="2.625" fill="#FF69B4" />
|
|
||||||
<g fill="url(#text-gradient)">
|
|
||||||
<circle cx="9" cy="3" r="1.875" />
|
|
||||||
<circle cx="14.625" cy="7.875" r="1.875" />
|
|
||||||
<circle cx="2.625" cy="4.875" r="1.875" />
|
|
||||||
<circle cx="4.125" cy="15.375" r="1.875" />
|
|
||||||
<circle cx="12.75" cy="14.625" r="1.875" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<text x="600" y="340" font-family="system-ui, -apple-system, sans-serif" font-size="80" font-weight="900" text-anchor="middle" fill="url(#text-gradient)">
|
|
||||||
AWESOME
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<!-- Subtitle -->
|
|
||||||
<text x="600" y="400" font-family="system-ui, -apple-system, sans-serif" font-size="32" font-weight="600" text-anchor="middle" fill="#666666">
|
|
||||||
Curated Lists Explorer
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<g transform="translate(600, 440)">
|
|
||||||
<text x="-250" y="0" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" text-anchor="middle" fill="#9733EE">
|
|
||||||
209 Lists
|
|
||||||
</text>
|
|
||||||
<text x="0" y="0" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" text-anchor="middle" fill="#FF69B4">
|
|
||||||
14K+ Repos
|
|
||||||
</text>
|
|
||||||
<text x="250" y="0" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="700" text-anchor="middle" fill="#FFD700">
|
|
||||||
FTS5 Search
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.3 KiB |
BIN
public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 84 KiB |