fix: icons

This commit is contained in:
valknarness
2025-10-26 22:53:16 +01:00
parent 8fc4aa8dab
commit 51b42fe625
32 changed files with 947 additions and 477 deletions

49
.env.example Normal file
View 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
View File

@@ -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
View 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.

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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>
)
}

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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}

View File

@@ -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.

View File

@@ -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

View File

@@ -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<

View File

@@ -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}

View File

@@ -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"

View File

@@ -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",

View File

@@ -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
View 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
View 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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB