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
|
||||
docker pull ghcr.io/valknarness/awesome-app:latest
|
||||
docker run -p 3000:3000 ghcr.io/valknarness/awesome-app:latest
|
||||
# Copy environment example
|
||||
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
|
||||
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.
|
||||
|
||||
### 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
|
||||
### Step 2: Create Data Directory
|
||||
|
||||
```bash
|
||||
docker build --build-arg INCLUDE_DATABASE=true -t awesome-app .
|
||||
docker run -p 3000:3000 awesome-app
|
||||
# Create directory for database
|
||||
sudo mkdir -p /var/lib/awesome/data
|
||||
sudo chown -R 1001:1001 /var/lib/awesome/data
|
||||
```
|
||||
|
||||
### Option 2: Volume-Mounted Database (Local Default)
|
||||
|
||||
**Pros:**
|
||||
- Smaller image size
|
||||
- Database can be updated independently
|
||||
- Easier for development
|
||||
|
||||
**Cons:**
|
||||
- Requires database setup/volume mount
|
||||
- Extra configuration needed
|
||||
### Step 3: Deploy
|
||||
|
||||
```bash
|
||||
docker build --build-arg INCLUDE_DATABASE=false -t awesome-app .
|
||||
docker run -p 3000:3000 -v $(pwd)/data:/app/data awesome-app
|
||||
# Pull latest image
|
||||
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
|
||||
services:
|
||||
awesome-app:
|
||||
build:
|
||||
args:
|
||||
INCLUDE_DATABASE: false # Change to true to embed database
|
||||
volumes:
|
||||
- ./data:/app/data # Only needed when INCLUDE_DATABASE=false
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '2'
|
||||
memory: 2G
|
||||
reservations:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
---
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `NODE_ENV` | `production` | Node.js environment |
|
||||
| `PORT` | `3000` | Application port |
|
||||
| `HOSTNAME` | `0.0.0.0` | Bind hostname |
|
||||
## 📚 Additional Resources
|
||||
|
||||
## 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.
|
||||
|
||||
## 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)
|
||||
**Built with 💜💗💛 and maximum awesomeness!**
|
||||
|
||||
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 *));
|
||||
|
||||
@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 */
|
||||
.gradient-text, .prose h1 {
|
||||
background: var(--gradient-awesome);
|
||||
@@ -224,11 +237,6 @@ kbd {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Focus Ring */
|
||||
*:focus-visible {
|
||||
@apply outline-none ring-2 ring-primary ring-offset-2;
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
@keyframes spin-awesome {
|
||||
from {
|
||||
|
||||
@@ -29,9 +29,8 @@ export const metadata: Metadata = {
|
||||
icons: {
|
||||
icon: [
|
||||
{ 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',
|
||||
},
|
||||
openGraph: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as React from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
CommandDialog,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from '@/components/ui/command'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Search, Star, BookOpen, Home, FileText, Code } from 'lucide-react'
|
||||
|
||||
interface CommandMenuProps {
|
||||
@@ -18,20 +19,34 @@ interface CommandMenuProps {
|
||||
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) {
|
||||
const router = useRouter()
|
||||
const [search, setSearch] = React.useState('')
|
||||
const [results, setResults] = React.useState([])
|
||||
const [results, setResults] = React.useState<SearchResult[]>([])
|
||||
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(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
||||
@@ -45,13 +60,53 @@ const fetchData = React.useCallback(async () => {
|
||||
}, [open, setOpen])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!search) {
|
||||
// Clear results if search is empty
|
||||
if (!search || search.trim() === '') {
|
||||
setResults([])
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
fetchData()
|
||||
console.log(results)
|
||||
setLoading(false)
|
||||
|
||||
// Debounce search
|
||||
const timer = setTimeout(async () => {
|
||||
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])
|
||||
|
||||
const runCommand = React.useCallback((command: () => void) => {
|
||||
@@ -94,13 +149,19 @@ const fetchData = React.useCallback(async () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput
|
||||
placeholder="Search awesome lists, repos, and more..."
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="overflow-hidden p-0" aria-describedby={undefined}>
|
||||
<DialogTitle className="sr-only">Search</DialogTitle>
|
||||
<Command
|
||||
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"
|
||||
>
|
||||
<CommandInput
|
||||
placeholder="Search awesome lists, repos, and more..."
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
@@ -133,17 +194,17 @@ const fetchData = React.useCallback(async () => {
|
||||
|
||||
{results.length > 0 && (
|
||||
<CommandGroup heading="Search Results">
|
||||
{results.map((result: any) => (
|
||||
{results.map((result) => (
|
||||
<CommandItem
|
||||
key={result.repository_id}
|
||||
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 items-center gap-2">
|
||||
<span className="font-medium">{result.title}</span>
|
||||
{result.stars && (
|
||||
<span className="font-medium">{result.repository_name}</span>
|
||||
{result.stars !== null && (
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Star className="h-3 w-3 fill-current" />
|
||||
{result.stars.toLocaleString()}
|
||||
@@ -155,17 +216,26 @@ const fetchData = React.useCallback(async () => {
|
||||
{result.description}
|
||||
</span>
|
||||
)}
|
||||
{result.category && (
|
||||
<span className="text-xs text-primary">
|
||||
{result.category}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{result.language && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{result.language}
|
||||
</span>
|
||||
)}
|
||||
{result.awesome_list_category && (
|
||||
<span className="text-xs text-primary">
|
||||
{result.awesome_list_category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
@@ -26,7 +26,8 @@ Command.displayName = CommandPrimitive.displayName
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<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">
|
||||
{children}
|
||||
</Command>
|
||||
@@ -44,7 +45,7 @@ const CommandInput = React.forwardRef<
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -115,7 +116,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
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",
|
||||
|
||||
// Variants based on alignment.
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
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]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
|
||||
@@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||
|
||||
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<
|
||||
|
||||
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -734,7 +734,7 @@ export const EditorProvider = ({
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className={cn(className, '[&_.ProseMirror-focused]:outline-none')}>
|
||||
<div className={cn(className, '[&_.ProseMirror-focused]:outline-hidden')}>
|
||||
<TiptapEditorProvider
|
||||
editorProps={{
|
||||
handleKeyDown: (_view, event) => {
|
||||
@@ -1374,7 +1374,7 @@ export const EditorLinkSelector = ({
|
||||
<form className="flex p-1" onSubmit={handleSubmit}>
|
||||
<input
|
||||
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 ?? ''}
|
||||
onChange={(event) => setUrl(event.target.value)}
|
||||
placeholder="Paste a link"
|
||||
|
||||
@@ -449,7 +449,7 @@ const SidebarGroupLabel = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
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",
|
||||
className
|
||||
)}
|
||||
@@ -470,7 +470,7 @@ const SidebarGroupAction = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
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.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
@@ -522,7 +522,7 @@ const SidebarMenuItem = React.forwardRef<
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem"
|
||||
|
||||
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: {
|
||||
variant: {
|
||||
@@ -616,7 +616,7 @@ const SidebarMenuAction = React.forwardRef<
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
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.
|
||||
"after:absolute after:-inset-2 after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
@@ -732,7 +732,7 @@ const SidebarMenuSubButton = React.forwardRef<
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
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",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
|
||||
@@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
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
|
||||
)}
|
||||
{...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",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icon-192.svg",
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.svg",
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/apple-touch-icon.svg",
|
||||
"sizes": "180x180",
|
||||
"type": "image/svg+xml",
|
||||
"type": "image/png",
|
||||
"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 |