diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7bbdc28 --- /dev/null +++ b/.env.example @@ -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 diff --git a/DOCKER.md b/DOCKER.md index 077ee8c..aca8eda 100644 --- a/DOCKER.md +++ b/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!** diff --git a/PRODUCTION_SETUP.md b/PRODUCTION_SETUP.md new file mode 100644 index 0000000..c1bc945 --- /dev/null +++ b/PRODUCTION_SETUP.md @@ -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 --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. diff --git a/app/globals.css b/app/globals.css index 76f56bd..0e0228e 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 { diff --git a/app/layout.tsx b/app/layout.tsx index 4868f21..99e1bc7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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: { diff --git a/components/layout/command-menu.tsx b/components/layout/command-menu.tsx index 0e1ccab..d01a57f 100644 --- a/components/layout/command-menu.tsx +++ b/components/layout/command-menu.tsx @@ -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([]) 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 ( - - - + + + Search + + + {loading ? (
@@ -133,17 +194,17 @@ const fetchData = React.useCallback(async () => { {results.length > 0 && ( - {results.map((result: any) => ( + {results.map((result) => ( runCommand(() => router.push(result.url))} + onSelect={() => runCommand(() => router.push(`/repository/${result.repository_id}`))} > - {getIcon(result.type)} +
- {result.title} - {result.stars && ( + {result.repository_name} + {result.stars !== null && ( {result.stars.toLocaleString()} @@ -155,17 +216,26 @@ const fetchData = React.useCallback(async () => { {result.description} )} - {result.category && ( - - {result.category} - - )} +
+ {result.language && ( + + {result.language} + + )} + {result.awesome_list_category && ( + + {result.awesome_list_category} + + )} +
))} )} - - + + + +
) } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx index e87d62b..e725b7a 100644 --- a/components/ui/badge.tsx +++ b/components/ui/badge.tsx @@ -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: { diff --git a/components/ui/button.tsx b/components/ui/button.tsx index 21409a0..fd6338f 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -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: { diff --git a/components/ui/command.tsx b/components/ui/command.tsx index 2cecd91..eca2823 100644 --- a/components/ui/command.tsx +++ b/components/ui/command.tsx @@ -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, @@ -26,7 +26,8 @@ Command.displayName = CommandPrimitive.displayName const CommandDialog = ({ children, ...props }: DialogProps) => { return ( - + + Command Menu {children} @@ -44,7 +45,7 @@ const CommandInput = React.forwardRef< ) { 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. diff --git a/components/ui/input.tsx b/components/ui/input.tsx index 8916905..8d55bcf 100644 --- a/components/ui/input.tsx +++ b/components/ui/input.tsx @@ -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 diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx index 4d000ec..73c9671 100644 --- a/components/ui/navigation-menu.tsx +++ b/components/ui/navigation-menu.tsx @@ -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< diff --git a/components/ui/select.tsx b/components/ui/select.tsx index 5f501dc..f8baefa 100644 --- a/components/ui/select.tsx +++ b/components/ui/select.tsx @@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef< 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< -
+
{ @@ -1374,7 +1374,7 @@ export const EditorLinkSelector = ({
setUrl(event.target.value)} placeholder="Paste a link" diff --git a/components/ui/sidebar.tsx b/components/ui/sidebar.tsx index d437b6c..8da7516 100644 --- a/components/ui/sidebar.tsx +++ b/components/ui/sidebar.tsx @@ -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", diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx index 7f21b5e..589aa11 100644 --- a/components/ui/textarea.tsx +++ b/components/ui/textarea.tsx @@ -7,7 +7,7 @@ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {