diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..eaed49e --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1.4 +FROM mcr.microsoft.com/devcontainers/typescript-node:20-bullseye + +# Install system dependencies in a single layer +RUN apt-get update && apt-get install -y --no-install-recommends \ + postgresql-client \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /workspaces/gitpodflix + +# Install global dependencies first +RUN npm install -g nodemon + +# Create directories for package files +RUN mkdir -p frontend backend/catalog + +# Copy package files for dependency caching +COPY frontend/package*.json ./frontend/ +COPY backend/catalog/package*.json ./backend/catalog/ + +# Install dependencies using BuildKit cache +RUN --mount=type=cache,target=/root/.npm \ + cd frontend && npm ci --prefer-offline --no-audit --no-fund && \ + cd ../backend/catalog && npm ci --prefer-offline --no-audit --no-fund + +# Copy the rest of the application +COPY . . + +# Set the default user +USER node diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c0097de..e5dfd80 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,26 +1,26 @@ { "name": "GitpodFlix Dev Environment", - "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "options": [ + "--build-arg", "BUILDKIT_INLINE_CACHE=1" + ] + }, "features": { "ghcr.io/devcontainers/features/common-utils:2": { "installZsh": true, "configureZshAsDefaultShell": true, - "installGit": true, - "installGitLFS": true, - "installGithubCli": true - }, - "ghcr.io/devcontainers/features/github-cli:1": { }, - "ghcr.io/devcontainers/features/node:1": { - "version": "18", - "nodeGypDependencies": true + "installGit": false, + "installGitLFS": false, + "installGithubCli": false }, + "ghcr.io/devcontainers/features/github-cli:1": {}, "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}, "ghcr.io/devcontainers/features/docker-in-docker:2": { - "version": "latest", - "moby": true - }, - "ghcr.io/devcontainers-extra/features/renovate-cli:2": {}, - "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {} + "version": "20.10", + "moby": false + } }, "forwardPorts": [ 3000, @@ -28,26 +28,26 @@ 5432 ], "postCreateCommand": ".devcontainer/setup.sh", + "postStartCommand": "./startup.sh", "customizations": { "vscode": { "extensions": [ - - // Install AI code assistants "GitHub.copilot", "RooVeterinaryInc.roo-cline", "saoudrizwan.claude-dev", - - // Install other extensions "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "mtxr.sqltools", "mtxr.sqltools-driver-sqlite", - "mtxr.sqltools-driver-mysql", + "mtxr.sqltools-driver-pg", "bradlc.vscode-tailwindcss" - ], "settings": { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "typescript.preferences.importModuleSpecifier": "relative" } } }, @@ -63,5 +63,11 @@ "label": "PostgreSQL" } }, - "remoteUser": "root" + "remoteUser": "node", + "mounts": [ + "source=gitpodflix-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume", + "source=gitpodflix-frontend-node-modules,target=${containerWorkspaceFolder}/frontend/node_modules,type=volume", + "source=gitpodflix-catalog-node-modules,target=${containerWorkspaceFolder}/backend/catalog/node_modules,type=volume", + "source=gitpodflix-npm-cache,target=/root/.npm,type=volume" + ] } diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh index 1030e43..037c7a2 100755 --- a/.devcontainer/setup.sh +++ b/.devcontainer/setup.sh @@ -1,80 +1,83 @@ #!/bin/bash -# Exit on error set -e echo "πŸš€ Starting development environment setup..." -# Function to handle package installation -install_package() { - local package=$1 - echo "πŸ“¦ Installing $package..." - if ! dpkg -l | grep -q "^ii $package "; then - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$package" - else - echo "βœ… $package is already installed" - fi -} - -# Clean apt cache and update package lists -echo "🧹 Cleaning apt cache..." -apt-get clean -echo "πŸ”„ Updating package lists..." -apt-get update - -# Install system dependencies one by one with error handling -echo "πŸ“¦ Installing system dependencies..." -install_package "mariadb-client" -install_package "mariadb-server" -install_package "postgresql-client" - -# Verify PostgreSQL client tools are installed +# Verify PostgreSQL client tools are installed (already installed in Dockerfile) if ! command -v pg_isready &> /dev/null; then echo "❌ PostgreSQL client tools not properly installed" exit 1 fi -# Start MariaDB service -echo "πŸ’Ύ Starting MariaDB service..." -if ! service mariadb status > /dev/null 2>&1; then - service mariadb start -else - echo "βœ… MariaDB service is already running" -fi - -# Verify MariaDB is running -if ! service mariadb status > /dev/null 2>&1; then - echo "❌ Failed to start MariaDB service" - exit 1 -fi - -# Install global npm packages -echo "πŸ“¦ Installing global npm packages..." -npm install -g nodemon - -# Install project dependencies -echo "πŸ“¦ Installing project dependencies..." - -# Install Gitpod Flix dependencies -if [ -d "/workspaces/gitpodflix-demo/frontend" ]; then - echo "πŸ“¦ Installing Gitpod Flix dependencies..." - cd /workspaces/gitpodflix-demo/frontend - npm install +# Install jq if not present (for health checks) +if ! command -v jq &> /dev/null; then + echo "πŸ“¦ Installing jq for JSON processing..." + sudo apt-get update && sudo apt-get install -y jq fi -# Install catalog service dependencies -if [ -d "/workspaces/gitpodflix-demo/backend/catalog" ]; then - echo "πŸ“¦ Installing catalog service dependencies..." - cd /workspaces/gitpodflix-demo/backend/catalog - npm install -fi +# Make scripts executable +chmod +x startup.sh 2>/dev/null || true +chmod +x health-check.sh 2>/dev/null || true echo "βœ… Setup completed successfully!" +# GitHub CLI authentication (optional) if [ -n "$GH_CLI_TOKEN" ]; then gh auth login --with-token <<< "$GH_CLI_TOKEN" - # Configure git to use GitHub CLI credentials gh auth setup-git else - echo "GH_CLI_TOKEN not set, skipping authentication" + echo "ℹ️ GH_CLI_TOKEN not set, skipping authentication" fi + +echo "πŸ”§ Available commands:" +echo " ./startup.sh - Start all services" +echo " ./health-check.sh - Check service health" + +# Setup Jira MCP server +echo "πŸš€ Setting up Jira MCP server..." + +# Create config directory +mkdir -p ~/.config/gitpod + +# Clone and build Jira MCP if not already present +if [ ! -d "/home/node/jira-mcp" ]; then + echo "πŸ“¦ Cloning Jira MCP repository..." + cd /home/node + git clone https://github.com/MankowskiNick/jira-mcp.git + cd jira-mcp + echo "πŸ“¦ Installing dependencies..." + npm install + echo "πŸ”¨ Building project..." + npm run build +else + echo "βœ“ Jira MCP already installed" +fi + +# Create MCP configuration file +echo "βš™οΈ Creating MCP configuration..." +cat > ~/.config/gitpod/mcp-config.json << EOF +{ + "mcpServers": { + "jira-mcp": { + "command": "node", + "args": ["/home/node/jira-mcp/build/index.js"], + "env": { + "JIRA_HOST": "${JIRA_HOST:-coakley.atlassian.net}", + "JIRA_USERNAME": "${JIRA_USERNAME:-joe@gitpod.io}", + "JIRA_API_TOKEN": "${JIRA_API_TOKEN:-your_api_token_here}", + "JIRA_PROJECT_KEY": "${JIRA_PROJECT_KEY:-MBA}", + "AUTO_CREATE_TEST_TICKETS": "true", + "JIRA_ACCEPTANCE_CRITERIA_FIELD": "customfield_10429", + "JIRA_STORY_POINTS_FIELD": "customfield_10040", + "JIRA_EPIC_LINK_FIELD": "customfield_10014" + } + } + } +} +EOF + +echo "βœ… Jira MCP server setup complete!" +echo "πŸ“ Configuration: ~/.config/gitpod/mcp-config.json" +echo "πŸ“ Server location: /home/node/jira-mcp/" +echo "🎯 Project: MBA (coakley.atlassian.net)" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d55c0aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,98 @@ +# Dependencies +node_modules +*/node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build outputs +dist +build +.next +.nuxt +.vite + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# IDE and editor files +.vscode +.idea +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# Temporary folders +tmp +temp + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.gitpod/automations.yaml b/.gitpod/automations.yaml index 3c130a2..c7e6b45 100644 --- a/.gitpod/automations.yaml +++ b/.gitpod/automations.yaml @@ -1,4 +1,59 @@ services: + jira-mcp: + name: "Jira MCP Server" + description: "Model Context Protocol server for Jira integration" + triggeredBy: + - postDevcontainerStart + - postEnvironmentStart + commands: + start: | + echo "Setting up Jira MCP server..." + + # Clone and build Jira MCP if not already present + if [ ! -d "/home/node/jira-mcp" ]; then + echo "Cloning Jira MCP repository..." + cd /home/node + git clone https://github.com/MankowskiNick/jira-mcp.git + cd jira-mcp + echo "Installing dependencies..." + npm install + echo "Building project..." + npm run build + else + echo "Jira MCP already installed" + fi + + # Test MCP server connection if credentials are available + if [ -n "\$JIRA_API_TOKEN" ] && [ "\$JIRA_API_TOKEN" != "your_api_token_here" ]; then + echo "Testing MCP server connection..." + cd /home/node/jira-mcp + + # Test server initialization + if echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {"roots": {"listChanged": true}}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}}' | timeout 10 node build/index.js >/dev/null 2>&1; then + echo "MCP server connection test successful" + else + echo "MCP server connection test failed - check credentials" + fi + fi + + echo "Jira MCP server setup complete" + echo "Server location: /home/node/jira-mcp/" + echo "Configuration: Uses standardized MCP config files (.mcp/config.json, mcp.json, .mcp.yaml)" + echo "Project: MBA (coakley.atlassian.net)" + echo "Available tools: create-ticket, get-ticket, search-tickets, update-ticket, link-tickets, get-test-steps, add-test-steps" + + ready: | + if [ -f "/home/node/jira-mcp/build/index.js" ]; then + echo "Jira MCP server is ready" + exit 0 + else + echo "Jira MCP server not ready" + exit 1 + fi + + stop: | + echo "Jira MCP server stopped (no persistent process)" + postgres: name: "PostgreSQL Database" description: "PostgreSQL database for Gitpod Flix (Port: 5432)" @@ -7,24 +62,43 @@ services: - postEnvironmentStart commands: start: | - # Force cleanup of any existing PostgreSQL containers + cd /workspaces/gitpodflix-demo/database/main + + # Clean up any existing containers that might conflict echo "Cleaning up any existing PostgreSQL containers..." - docker rm -f postgres 2>/dev/null || true - sleep 2 # Give Docker time to clean up - - echo "Starting PostgreSQL with Docker..." - docker run --rm \ - --name postgres \ - -e POSTGRES_USER=gitpod \ - -e POSTGRES_PASSWORD=gitpod \ - -e POSTGRES_DB=gitpodflix \ - -p 5432:5432 \ - -v postgres_data:/var/lib/postgresql/data \ - -v /workspaces/gitpodflix-demo/database/main/migrations:/docker-entrypoint-initdb.d \ - postgres:15 + docker stop main-postgres-1 2>/dev/null || true + docker rm main-postgres-1 2>/dev/null || true + docker stop postgres 2>/dev/null || true + docker rm postgres 2>/dev/null || true + + # Kill any process using port 5432 + if lsof -Pi :5432 -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "Killing process on port 5432..." + kill -9 $(lsof -ti:5432) 2>/dev/null || true + sleep 3 + fi + + echo "Starting PostgreSQL with docker-compose..." + docker-compose up -d + + # Wait for container to be healthy + echo "Waiting for PostgreSQL to be healthy..." + for i in {1..30}; do + if docker-compose ps | grep -q "healthy"; then + echo "PostgreSQL container is healthy" + break + fi + if [ $i -eq 30 ]; then + echo "Timeout waiting for PostgreSQL to be healthy" + docker-compose logs + exit 1 + fi + echo "Waiting... attempt $i/30" + sleep 2 + done ready: | - if docker exec postgres pg_isready -U gitpod; then + if PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -c "SELECT 1" >/dev/null 2>&1; then echo "PostgreSQL is ready and accepting connections" exit 0 else @@ -33,10 +107,9 @@ services: fi stop: | - echo "Stopping PostgreSQL container..." - docker stop postgres || true - echo "Removing PostgreSQL container..." - docker rm -f postgres || true + cd /workspaces/gitpodflix-demo/database/main + echo "Stopping PostgreSQL with docker-compose..." + docker-compose down catalog: name: "Catalog Service" @@ -48,26 +121,47 @@ services: start: | cd /workspaces/gitpodflix-demo/backend/catalog + # Kill any existing process on port 3001 + if lsof -Pi :3001 -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "Killing existing process on port 3001..." + kill -9 $(lsof -ti:3001) 2>/dev/null || true + sleep 2 + fi + + # Create .env file if it doesn't exist + if [ ! -f .env ]; then + echo "Creating .env file..." + cat > .env << EOF + DB_HOST=localhost + DB_USER=gitpod + DB_PASSWORD=gitpod + DB_NAME=gitpodflix + DB_PORT=5432 + PORT=3001 + EOF + fi + # Ensure dependencies are installed - echo "Ensuring dependencies are installed..." + echo "Installing dependencies..." npm install - # Wait for ts-node to be available - echo "Waiting for ts-node to be available..." - for i in {1..30}; do - if [ -f "node_modules/.bin/ts-node" ]; then - echo "βœ“ ts-node is available" + # Wait for PostgreSQL to be ready before starting + echo "Waiting for PostgreSQL to be ready..." + for i in {1..60}; do + if PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -c "SELECT 1" >/dev/null 2>&1; then + echo "PostgreSQL is ready" break fi - if [ $i -eq 30 ]; then - echo "βœ— Timeout waiting for ts-node" + if [ $i -eq 60 ]; then + echo "Timeout waiting for PostgreSQL" exit 1 fi - echo "Waiting for ts-node... attempt $i/30" - sleep 1 + echo "Waiting for PostgreSQL... attempt $i/60" + sleep 2 done - PORT=3001 npm run dev + echo "Starting catalog service..." + npm run dev ready: | if curl -s http://localhost:3001/health > /dev/null; then echo "Catalog service is ready" @@ -77,6 +171,9 @@ services: exit 1 fi stop: | + if lsof -Pi :3001 -sTCP:LISTEN -t >/dev/null 2>&1; then + kill -9 $(lsof -ti:3001) 2>/dev/null || true + fi pkill -f "node.*catalog" || true gitpod-flix: @@ -89,22 +186,34 @@ services: start: | cd /workspaces/gitpodflix-demo/frontend - # Wait for vite to be available - echo "Waiting for vite to be available..." - for i in {1..30}; do - if command -v vite >/dev/null 2>&1 || [ -f "node_modules/.bin/vite" ]; then - echo "βœ“ vite is available" + # Kill any existing process on port 3000 + if lsof -Pi :3000 -sTCP:LISTEN -t >/dev/null 2>&1; then + echo "Killing existing process on port 3000..." + kill -9 $(lsof -ti:3000) 2>/dev/null || true + sleep 2 + fi + + # Install dependencies + echo "Installing frontend dependencies..." + npm install + + # Wait for backend to be ready before starting frontend + echo "Waiting for backend API to be ready..." + for i in {1..60}; do + if curl -s http://localhost:3001/health >/dev/null 2>&1; then + echo "Backend API is ready" break fi - if [ $i -eq 30 ]; then - echo "βœ— Timeout waiting for vite" + if [ $i -eq 60 ]; then + echo "Timeout waiting for backend API" exit 1 fi - echo "Waiting for vite... attempt $i/30" + echo "Waiting for backend... attempt $i/60" sleep 2 done - PORT=3000 npm run dev + echo "Starting frontend service..." + npm run dev ready: | if curl -s http://localhost:3000 > /dev/null; then echo "Gitpod Flix is ready" @@ -114,9 +223,120 @@ services: exit 1 fi stop: | + if lsof -Pi :3000 -sTCP:LISTEN -t >/dev/null 2>&1; then + kill -9 $(lsof -ti:3000) 2>/dev/null || true + fi pkill -f "node.*frontend" || true + pkill -f "vite" || true tasks: + setupJiraMCP: + name: "Setup Jira MCP" + description: "Install and configure Jira MCP server for AI integration" + triggeredBy: + - manual + - postEnvironmentStart + command: | + echo "Setting up Jira MCP server..." + + # Create config directory + mkdir -p ~/.config/gitpod + + # Clone and build Jira MCP if not already present + if [ ! -d "/home/node/jira-mcp" ]; then + echo "Cloning Jira MCP repository..." + cd /home/node + git clone https://github.com/MankowskiNick/jira-mcp.git + cd jira-mcp + echo "Installing dependencies..." + npm install + echo "Building project..." + npm run build + else + echo "Jira MCP already installed" + fi + + # Create MCP configuration file + echo "Creating MCP configuration..." + cat > ~/.config/gitpod/mcp-config.json << EOF + { + "mcpServers": { + "jira-mcp": { + "command": "node", + "args": ["/home/node/jira-mcp/build/index.js"], + "env": { + "JIRA_HOST": "\${JIRA_HOST:-coakley.atlassian.net}", + "JIRA_USERNAME": "\${JIRA_USERNAME:-joe@gitpod.io}", + "JIRA_API_TOKEN": "\${JIRA_API_TOKEN:-your_api_token_here}", + "JIRA_PROJECT_KEY": "\${JIRA_PROJECT_KEY:-MBA}", + "AUTO_CREATE_TEST_TICKETS": "true", + "JIRA_ACCEPTANCE_CRITERIA_FIELD": "customfield_10429", + "JIRA_STORY_POINTS_FIELD": "customfield_10040", + "JIRA_EPIC_LINK_FIELD": "customfield_10014" + } + } + } + } + EOF + + # Configure generic MCP client connection + echo "Configuring MCP client connection..." + + # Create generic MCP client config + MCP_CLIENT_CONFIG="$HOME/.config/mcp/client-config.json" + mkdir -p "$HOME/.config/mcp" + cat > "$MCP_CLIENT_CONFIG" << EOF + { + "mcpServers": { + "jira-mcp": { + "command": "node", + "args": ["/home/node/jira-mcp/build/index.js"], + "env": { + "JIRA_HOST": "\${JIRA_HOST:-coakley.atlassian.net}", + "JIRA_USERNAME": "\${JIRA_USERNAME:-joe@gitpod.io}", + "JIRA_API_TOKEN": "\${JIRA_API_TOKEN:-your_api_token_here}", + "JIRA_PROJECT_KEY": "\${JIRA_PROJECT_KEY:-MBA}", + "AUTO_CREATE_TEST_TICKETS": "true", + "JIRA_ACCEPTANCE_CRITERIA_FIELD": "customfield_10429", + "JIRA_STORY_POINTS_FIELD": "customfield_10040", + "JIRA_EPIC_LINK_FIELD": "customfield_10014" + } + } + } + } + EOF + + # Test the MCP server if credentials are available + if [ -n "\$JIRA_API_TOKEN" ] && [ "\$JIRA_API_TOKEN" != "your_api_token_here" ]; then + echo "Testing MCP server connection..." + cd /home/node/jira-mcp + + # Test server initialization + if echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {"roots": {"listChanged": true}}, "clientInfo": {"name": "test-client", "version": "1.0.0"}}}' | timeout 10 node build/index.js >/dev/null 2>&1; then + echo "MCP server connection test successful" + else + echo "MCP server connection test failed - check credentials" + fi + fi + + # Create a simple verification script + echo '#!/bin/bash' > /tmp/verify-mcp-connection.sh + echo 'echo "Verifying MCP configuration..."' >> /tmp/verify-mcp-connection.sh + echo 'echo "Server config: $([ -f ~/.config/gitpod/mcp-config.json ] && echo \"Found\" || echo \"Missing\")"' >> /tmp/verify-mcp-connection.sh + echo 'echo "Generic client config: $([ -f ~/.config/mcp/client-config.json ] && echo \"Found\" || echo \"Missing\")"' >> /tmp/verify-mcp-connection.sh + echo 'echo "MCP server binary: $([ -f /home/node/jira-mcp/build/index.js ] && echo \"Found\" || echo \"Missing\")"' >> /tmp/verify-mcp-connection.sh + chmod +x /tmp/verify-mcp-connection.sh + + echo "Jira MCP server and client setup complete" + echo "Server config: ~/.config/gitpod/mcp-config.json" + echo "Generic client config: ~/.config/mcp/client-config.json" + echo "Server location: /home/node/jira-mcp/" + echo "Project: MBA (coakley.atlassian.net)" + echo "Available tools: create-ticket, get-ticket, search-tickets, update-ticket, link-tickets, get-test-steps, add-test-steps" + echo "" + echo "Run '/tmp/verify-mcp-connection.sh' to verify the setup" + echo "AI assistants may need to restart to pick up the new MCP configuration" + seedDatabase: name: "Seed Database" description: "Seed the database with sample movies in a dramatic sequence" @@ -130,35 +350,31 @@ tasks: echo "Waiting for PostgreSQL to be ready..." for i in {1..30}; do if PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -c "SELECT 1" >/dev/null 2>&1; then - echo "βœ“ PostgreSQL is ready" + echo "PostgreSQL is ready" break fi if [ $i -eq 30 ]; then - echo "βœ— Timeout waiting for PostgreSQL" + echo "Timeout waiting for PostgreSQL" exit 1 fi echo "Waiting for PostgreSQL... attempt $i/30" sleep 2 done - echo "Clearing existing data..." - PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -c "TRUNCATE TABLE movies;" - - echo "Seeding trending movies..." - PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/01_seed_trending.sql - sleep 4 - - echo "Seeding popular movies..." - PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/02_seed_popular.sql - sleep 4 - - echo "Seeding classic movies..." - PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/03_seed_classics.sql - sleep 4 + echo "Seeding complete movie database with optimized structure..." + PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/movies_complete.sql + sleep 2 - echo "Seeding sci-fi movies..." - PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/04_seed_scifi.sql - echo "Database seeding complete!" + echo "Database seeding complete" + echo "Movies with categories: 18" + echo "Trending: 4 movies" + echo "Popular: 4 movies" + echo "Sci-Fi: 4 movies" + echo "Award Winners: 4 movies" + echo "Modern Blockbusters: 4 movies" + echo "All movies have complete metadata (genres, trailers, cast, etc.)" + echo "Fixed Moonlight image URL" + echo "Optimized single-file structure" clearDatabase: name: "Clear Database" diff --git a/.mcp.yaml b/.mcp.yaml new file mode 100644 index 0000000..9c87701 --- /dev/null +++ b/.mcp.yaml @@ -0,0 +1,28 @@ +mcpServers: + jira-mcp: + command: node + args: + - /home/node/jira-mcp/build/index.js + env: + JIRA_HOST: ${JIRA_HOST:-coakley.atlassian.net} + JIRA_USERNAME: ${JIRA_USERNAME:-joe@gitpod.io} + JIRA_API_TOKEN: ${JIRA_API_TOKEN:-your_api_token_here} + JIRA_PROJECT_KEY: ${JIRA_PROJECT_KEY:-MBA} + AUTO_CREATE_TEST_TICKETS: "true" + JIRA_ACCEPTANCE_CRITERIA_FIELD: customfield_10429 + JIRA_STORY_POINTS_FIELD: customfield_10040 + JIRA_EPIC_LINK_FIELD: customfield_10014 + +tools: + - create-ticket + - get-ticket + - search-tickets + - update-ticket + - link-tickets + - get-test-steps + - add-test-steps + +server_location: /home/node/jira-mcp/ +project: + key: MBA + host: coakley.atlassian.net diff --git a/.mcp/config.json b/.mcp/config.json new file mode 100644 index 0000000..dfea162 --- /dev/null +++ b/.mcp/config.json @@ -0,0 +1,32 @@ +{ + "mcpServers": { + "jira-mcp": { + "command": "node", + "args": ["/home/node/jira-mcp/build/index.js"], + "env": { + "JIRA_HOST": "${JIRA_HOST:-coakley.atlassian.net}", + "JIRA_USERNAME": "${JIRA_USERNAME:-joe@gitpod.io}", + "JIRA_API_TOKEN": "${JIRA_API_TOKEN:-your_api_token_here}", + "JIRA_PROJECT_KEY": "${JIRA_PROJECT_KEY:-MBA}", + "AUTO_CREATE_TEST_TICKETS": "true", + "JIRA_ACCEPTANCE_CRITERIA_FIELD": "customfield_10429", + "JIRA_STORY_POINTS_FIELD": "customfield_10040", + "JIRA_EPIC_LINK_FIELD": "customfield_10014" + } + } + }, + "tools": [ + "create-ticket", + "get-ticket", + "search-tickets", + "update-ticket", + "link-tickets", + "get-test-steps", + "add-test-steps" + ], + "server_location": "/home/node/jira-mcp/", + "project": { + "key": "MBA", + "host": "coakley.atlassian.net" + } +} diff --git a/README.md b/README.md index 0451ae2..024bddd 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,273 @@ -# Welcome to GitpodFlix! 🎬 +# GitpodFlix 🎬 -Hey there new developer! πŸ‘‹ +A modern streaming platform demo built for Gitpod environments, showcasing a full-stack application with React frontend, Node.js backend, and PostgreSQL database. -Welcome to GitpodFlix, where we're providing the next generation of streaming experiences. +## Features -We're thrilled you joined, let's get you shipping today ! +- πŸŽ₯ Movie catalog with search and filtering +- πŸ” Advanced search with full-text capabilities +- πŸ“± Responsive design with Tailwind CSS +- πŸš€ Fast development with Vite -### At GitpodFlix: we ship to production on your first day +## Jira MCP Integration -We know happy developers that are in flow create better products and ship more value. +This Gitpod environment includes an automated Jira MCP (Model Context Protocol) server setup that allows AI assistants to interact with Jira for project management tasks. -At GitpodFlix we have **zero 'works on my machine' issues** because of **Gitpod**. Onboarding is **one-click to get a running environment with everything you need to ship new fixes and features today** which is why: +### Automated Setup -We expect every new developer to **ship to production on their first day**. +The Jira MCP server automatically installs and configures on environment startup: +- Clones from https://github.com/MankowskiNick/jira-mcp +- Installs to `/home/node/jira-mcp/` +- Creates configuration at `~/.config/gitpod/mcp-config.json` +- Pre-configured for coakley.atlassian.net MBA project -## Starting your development environment +### Available Tools -1. **Check your email** - * You should have an email invite to join the GitpodFlix organization +The MCP server provides the following tools: +- **create-ticket**: Create new Jira tickets with summary, description, acceptance criteria, and issue type +- **get-ticket**: Retrieve details of existing Jira tickets +- **search-tickets**: Search for tickets by issue type and additional criteria +- **update-ticket**: Update existing tickets with new field values +- **link-tickets**: Link tickets together with specified relationship types +- **get-test-steps**: Retrieve test steps from Zephyr test tickets +- **add-test-steps**: Add test steps to test tickets via Zephyr integration + +### Configuration Status + +βœ… **AUTOMATED SETUP CONFIGURED** + +The Jira MCP integration is now configured to automatically install and configure on every environment startup: + +#### Automatic Installation +- **Devcontainer Setup**: Jira MCP installs during `.devcontainer/setup.sh` +- **Gitpod Automations**: Additional automation task available via `setupJiraMCP` +- **Configuration**: Auto-creates `~/.config/gitpod/mcp-config.json` + +#### Current Configuration +- **Jira Instance**: coakley.atlassian.net +- **User**: joe@gitpod.io +- **Project**: MBA (Team #1 Issues) +- **Server Location**: `/home/node/jira-mcp/` +- **Config File**: `~/.config/gitpod/mcp-config.json` + +### Test Results + +βœ… Connection to Jira instance verified +βœ… Authentication successful +βœ… MCP server responds correctly +βœ… Automation setup tested and working +βœ… Configuration persists across environment restarts + +### Usage + +The MCP server automatically installs on environment startup and is ready to use with AI assistants that support the Model Context Protocol. You can: +- Create tickets in the MBA project +- Search existing tickets +- Update ticket information +- Link tickets together + +### Startup Process + +When a new environment starts: +1. `.devcontainer/setup.sh` runs automatically +2. Jira MCP repository is cloned to `/home/node/jira-mcp/` +3. Dependencies are installed and project is built +4. MCP configuration file is created with credentials +5. Server is ready for use by AI assistants + +### Manual Testing + +To test the server manually: +```bash +cd ~/jira-mcp +export JIRA_HOST="coakley.atlassian.net" JIRA_USERNAME="joe@gitpod.io" JIRA_API_TOKEN="..." JIRA_PROJECT_KEY="MBA" +echo '{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "search-tickets", "arguments": {"issue_type": "Task", "max_results": 5}}}' | node build/index.js +``` +- 🐳 Containerized PostgreSQL database +- πŸ”„ Real-time updates and suggestions +- ⚑ Automatic service startup and health monitoring + +## Architecture + +- **Frontend**: React 18 + TypeScript + Vite + Tailwind CSS (Port 3000) +- **Backend**: Node.js + Express + TypeScript (Port 3001) +- **Database**: PostgreSQL 15 with full-text search (Port 5432) +- **Development**: Hot reload, automated setup, health checks + +## Quick Start + +### Automatic Startup ✨ +The environment automatically starts all services when opened: + +1. **Wait for initialization** (30-60 seconds) +2. **Access the application**: + - **Frontend**: http://localhost:3000 + - **Backend API**: http://localhost:3001 + - **Health Check**: http://localhost:3001/health + +### Manual Control πŸ”§ +```bash +# Start all services +./startup.sh + +# Check service health +./health-check.sh + +# Restart if needed +./startup.sh +``` + +## Service Status + +Check if all services are running: + +```bash +./health-check.sh +``` + +Expected output: +``` +πŸ” GitpodFlix Health Check +========================== +[SUCCESS] PostgreSQL: βœ… Connected (18 movies in database) +[SUCCESS] Backend API: βœ… http://localhost:3001 (Status: OK) +[SUCCESS] Frontend: βœ… http://localhost:3000 (Gitpod Flix) +[SUCCESS] Movies API: βœ… /api/movies (18 movies) +[SUCCESS] Search API: βœ… /api/search (1 results for 'dark') + +πŸ“Š Health Check Summary +====================== +[SUCCESS] All services are healthy! πŸŽ‰ +``` + +## Development + +### Project Structure +``` +β”œβ”€β”€ frontend/ # React frontend application +β”œβ”€β”€ backend/catalog/ # Node.js API service +β”œβ”€β”€ database/main/ # PostgreSQL setup and migrations +β”œβ”€β”€ .devcontainer/ # Development container configuration +β”œβ”€β”€ .gitpod/ # Gitpod automation configuration +β”œβ”€β”€ startup.sh # πŸš€ Service startup script +β”œβ”€β”€ health-check.sh # πŸ” Health monitoring script +└── README.md # This file +``` + +### API Endpoints + +- `GET /api/movies` - Get all movies +- `GET /api/search` - Search movies with filters +- `GET /api/suggestions` - Get search suggestions +- `POST /api/movies/seed` - Seed database with sample data +- `GET /health` - Service health check + +### Database Schema + +The PostgreSQL database includes: +- Full-text search capabilities +- Movie metadata (genres, cast, director, etc.) +- Optimized indexes for search performance +- Sample movie data with complete metadata + +## Troubleshooting + +### Quick Diagnostics +```bash +# Check all services +./health-check.sh + +# Restart all services +./startup.sh +``` + +### Service Logs +```bash +# Database logs +docker logs main-postgres-1 + +# Backend logs +tail -f /tmp/catalog.log + +# Frontend logs +tail -f /tmp/frontend.log +``` + +### Manual Service Management +```bash +# Database +cd database/main && docker-compose up -d + +# Backend +cd backend/catalog && npm run dev + +# Frontend +cd frontend && npm run dev +``` + +### Common Issues & Solutions + +| Issue | Solution | +|-------|----------| +| **Port conflicts** | Services automatically kill existing processes | +| **Database not ready** | Wait 30s, then run `./health-check.sh` | +| **Missing dependencies** | Run `npm install` in respective directories | +| **Environment variables** | Check `.env` file in `backend/catalog/` | +| **Services not starting** | Run `./startup.sh` to restart all | + +## Features Demo + +### Search Functionality +- Full-text search across titles, descriptions, and directors +- Genre filtering (Action, Drama, Sci-Fi, etc.) +- Year range filtering (1970-2024) +- Rating and duration filters +- Real-time search suggestions + +### Movie Catalog +- Responsive grid layout +- Movie posters and metadata +- Sorting by rating, year, and relevance +- Pagination support +- 18 sample movies with complete metadata + +### API Features +- RESTful API design +- Advanced PostgreSQL queries with full-text search +- Error handling and validation +- Health monitoring endpoints +- Automatic database seeding + +## Development Tools + +- **VS Code Extensions**: Pre-configured for TypeScript, React, and PostgreSQL +- **Hot Reload**: Both frontend and backend support hot reloading +- **Database Tools**: SQLTools extension for database management +- **Linting**: ESLint and Prettier configured +- **Git Integration**: GitHub CLI and Git configured +- **Health Monitoring**: Automated service health checks + +## Environment Configuration + +The development environment includes: +- βœ… Node.js 18+ with npm +- βœ… PostgreSQL 15 client tools +- βœ… Docker for database containerization +- βœ… All necessary VS Code extensions +- βœ… Automated port forwarding (3000, 3001, 5432) +- βœ… Health monitoring and logging +- βœ… Automatic service startup on environment start + +## Contributing + +1. Make changes to the codebase +2. Test with `./health-check.sh` +3. Services automatically restart on file changes +4. Commit changes with descriptive messages + +## License + +This project is a demonstration application for Gitpod environments. * And a link for your first GitHub issue 3. **Go to the projects catalog** * Find it at: [app.gitpod.io/projects](https://app.gitpod.io/projects) @@ -131,7 +381,7 @@ tasks: - manual - postEnvironmentStart command: | - PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/01_seed_trending.sql + PGPASSWORD=gitpod psql -h localhost -U gitpod -d gitpodflix -f seeds/movies_complete.sql ``` This includes: diff --git a/backend/catalog/src/index.ts b/backend/catalog/src/index.ts index 3d711b1..d85565d 100644 --- a/backend/catalog/src/index.ts +++ b/backend/catalog/src/index.ts @@ -32,6 +32,232 @@ app.get('/api/movies', async (req, res) => { } }); +// Search endpoint with advanced filtering +app.get('/api/search', async (req, res) => { + try { + const { + q, + genres, + yearMin, + yearMax, + ratingMin, + ratingMax, + durationMin, + durationMax, + limit = 50, + offset = 0 + } = req.query; + + let query = 'SELECT * FROM movies WHERE 1=1'; + const params: any[] = []; + let paramIndex = 1; + + // Full-text search + if (q && typeof q === 'string' && q.trim()) { + query += ` AND ( + to_tsvector('english', title || ' ' || COALESCE(description, '') || ' ' || COALESCE(director, '')) + @@ plainto_tsquery('english', $${paramIndex}) + OR title ILIKE $${paramIndex + 1} + OR description ILIKE $${paramIndex + 1} + OR director ILIKE $${paramIndex + 1} + )`; + params.push(q.trim(), `%${q.trim()}%`); + paramIndex += 2; + } + + // Genre filtering + if (genres && typeof genres === 'string') { + const genreList = genres.split(',').map(g => g.trim()).filter(g => g); + if (genreList.length > 0) { + query += ` AND genres && $${paramIndex}`; + params.push(genreList); + paramIndex++; + } + } + + // Year range filtering + if (yearMin && !isNaN(Number(yearMin))) { + query += ` AND release_year >= $${paramIndex}`; + params.push(Number(yearMin)); + paramIndex++; + } + if (yearMax && !isNaN(Number(yearMax))) { + query += ` AND release_year <= $${paramIndex}`; + params.push(Number(yearMax)); + paramIndex++; + } + + // Rating range filtering + if (ratingMin && !isNaN(Number(ratingMin))) { + query += ` AND rating >= $${paramIndex}`; + params.push(Number(ratingMin)); + paramIndex++; + } + if (ratingMax && !isNaN(Number(ratingMax))) { + query += ` AND rating <= $${paramIndex}`; + params.push(Number(ratingMax)); + paramIndex++; + } + + // Duration range filtering + if (durationMin && !isNaN(Number(durationMin))) { + query += ` AND duration >= $${paramIndex}`; + params.push(Number(durationMin)); + paramIndex++; + } + if (durationMax && !isNaN(Number(durationMax))) { + query += ` AND duration <= $${paramIndex}`; + params.push(Number(durationMax)); + paramIndex++; + } + + // Add ordering and pagination + query += ` ORDER BY + CASE WHEN $1 IS NOT NULL THEN + ts_rank(to_tsvector('english', title || ' ' || COALESCE(description, '')), plainto_tsquery('english', $1)) + ELSE 0 END DESC, + rating DESC, + release_year DESC + LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; + + params.push(Number(limit), Number(offset)); + + const result = await pool.query(query, params); + + // Get total count for pagination + let countQuery = 'SELECT COUNT(*) FROM movies WHERE 1=1'; + const countParams: any[] = []; + let countParamIndex = 1; + + // Apply same filters for count + if (q && typeof q === 'string' && q.trim()) { + countQuery += ` AND ( + to_tsvector('english', title || ' ' || COALESCE(description, '') || ' ' || COALESCE(director, '')) + @@ plainto_tsquery('english', $${countParamIndex}) + OR title ILIKE $${countParamIndex + 1} + OR description ILIKE $${countParamIndex + 1} + OR director ILIKE $${countParamIndex + 1} + )`; + countParams.push(q.trim(), `%${q.trim()}%`); + countParamIndex += 2; + } + + if (genres && typeof genres === 'string') { + const genreList = genres.split(',').map(g => g.trim()).filter(g => g); + if (genreList.length > 0) { + countQuery += ` AND genres && $${countParamIndex}`; + countParams.push(genreList); + countParamIndex++; + } + } + + if (yearMin && !isNaN(Number(yearMin))) { + countQuery += ` AND release_year >= $${countParamIndex}`; + countParams.push(Number(yearMin)); + countParamIndex++; + } + if (yearMax && !isNaN(Number(yearMax))) { + countQuery += ` AND release_year <= $${countParamIndex}`; + countParams.push(Number(yearMax)); + countParamIndex++; + } + + if (ratingMin && !isNaN(Number(ratingMin))) { + countQuery += ` AND rating >= $${countParamIndex}`; + countParams.push(Number(ratingMin)); + countParamIndex++; + } + if (ratingMax && !isNaN(Number(ratingMax))) { + countQuery += ` AND rating <= $${countParamIndex}`; + countParams.push(Number(ratingMax)); + countParamIndex++; + } + + if (durationMin && !isNaN(Number(durationMin))) { + countQuery += ` AND duration >= $${countParamIndex}`; + countParams.push(Number(durationMin)); + countParamIndex++; + } + if (durationMax && !isNaN(Number(durationMax))) { + countQuery += ` AND duration <= $${countParamIndex}`; + countParams.push(Number(durationMax)); + countParamIndex++; + } + + const countResult = await pool.query(countQuery, countParams); + const totalCount = parseInt(countResult.rows[0].count); + + res.json({ + results: result.rows, + pagination: { + total: totalCount, + limit: Number(limit), + offset: Number(offset), + hasMore: Number(offset) + Number(limit) < totalCount + } + }); + } catch (err) { + console.error('Error searching movies:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Search suggestions endpoint +app.get('/api/suggestions', async (req, res) => { + try { + const { q } = req.query; + + if (!q || typeof q !== 'string' || q.trim().length < 2) { + return res.json([]); + } + + const searchTerm = q.trim(); + + // Get suggestions from titles, directors, and cast + const query = ` + SELECT DISTINCT suggestion, type, COUNT(*) as frequency + FROM ( + SELECT title as suggestion, 'title' as type FROM movies + WHERE title ILIKE $1 + UNION ALL + SELECT director as suggestion, 'director' as type FROM movies + WHERE director ILIKE $1 AND director IS NOT NULL + UNION ALL + SELECT unnest(cast) as suggestion, 'actor' as type FROM movies + WHERE cast IS NOT NULL AND EXISTS ( + SELECT 1 FROM unnest(cast) as actor WHERE actor ILIKE $1 + ) + UNION ALL + SELECT unnest(genres) as suggestion, 'genre' as type FROM movies + WHERE genres IS NOT NULL AND EXISTS ( + SELECT 1 FROM unnest(genres) as genre WHERE genre ILIKE $1 + ) + ) suggestions + GROUP BY suggestion, type + ORDER BY frequency DESC, suggestion ASC + LIMIT 10 + `; + + const result = await pool.query(query, [`%${searchTerm}%`]); + + const suggestions = result.rows.map(row => ({ + text: row.suggestion, + type: row.type, + frequency: parseInt(row.frequency) + })); + + res.json(suggestions); + } catch (err) { + console.error('Error fetching suggestions:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Health check endpoint +app.get('/health', (req, res) => { + res.json({ status: 'OK', timestamp: new Date().toISOString() }); +}); + app.post('/api/movies/seed', async (req, res) => { try { // Execute the seed script @@ -64,4 +290,4 @@ app.post('/api/movies/clear', async (req, res) => { // Start server app.listen(port, () => { console.log(`Catalog service running on port ${port}`); -}); \ No newline at end of file +}); diff --git a/database/main/docker-compose.yml b/database/main/docker-compose.yml index 96c16a7..a331df7 100644 --- a/database/main/docker-compose.yml +++ b/database/main/docker-compose.yml @@ -2,16 +2,30 @@ version: "3.8" services: postgres: - image: postgres:15 + image: postgres:15-alpine + restart: unless-stopped environment: POSTGRES_USER: gitpod POSTGRES_PASSWORD: gitpod POSTGRES_DB: gitpodflix + POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256" ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - - ./migrations:/docker-entrypoint-initdb.d + - ./migrations:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gitpod -d gitpodflix"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + security_opt: + - no-new-privileges:true + tmpfs: + - /tmp + - /var/run/postgresql volumes: postgres_data: + driver: local diff --git a/database/main/migrations/02_add_search_metadata.sql b/database/main/migrations/02_add_search_metadata.sql new file mode 100644 index 0000000..53d9a71 --- /dev/null +++ b/database/main/migrations/02_add_search_metadata.sql @@ -0,0 +1,22 @@ +-- Add search and filtering metadata to movies table +ALTER TABLE movies ADD COLUMN IF NOT EXISTS genres TEXT[]; +ALTER TABLE movies ADD COLUMN IF NOT EXISTS director VARCHAR(255); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS movie_cast TEXT[]; +ALTER TABLE movies ADD COLUMN IF NOT EXISTS duration INTEGER; -- in minutes +ALTER TABLE movies ADD COLUMN IF NOT EXISTS content_type VARCHAR(20) DEFAULT 'movie'; +ALTER TABLE movies ADD COLUMN IF NOT EXISTS keywords TEXT[]; +ALTER TABLE movies ADD COLUMN IF NOT EXISTS trailer_url VARCHAR(255); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS youtube_trailer_id VARCHAR(50); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS categories TEXT[]; + +-- Create indexes for better search performance +CREATE INDEX IF NOT EXISTS idx_movies_title_search ON movies USING gin(to_tsvector('english', title)); +CREATE INDEX IF NOT EXISTS idx_movies_description_search ON movies USING gin(to_tsvector('english', description)); +CREATE INDEX IF NOT EXISTS idx_movies_genres ON movies USING gin(genres); +CREATE INDEX IF NOT EXISTS idx_movies_release_year ON movies(release_year); +CREATE INDEX IF NOT EXISTS idx_movies_rating ON movies(rating); +CREATE INDEX IF NOT EXISTS idx_movies_duration ON movies(duration); + +-- Create a combined search index for full-text search +CREATE INDEX IF NOT EXISTS idx_movies_full_search ON movies +USING gin(to_tsvector('english', title || ' ' || COALESCE(description, '') || ' ' || COALESCE(director, ''))); diff --git a/database/main/migrations/03_add_trailer_urls.sql b/database/main/migrations/03_add_trailer_urls.sql new file mode 100644 index 0000000..5347762 --- /dev/null +++ b/database/main/migrations/03_add_trailer_urls.sql @@ -0,0 +1,29 @@ +-- Add trailer and video URL columns to movies table +ALTER TABLE movies ADD COLUMN IF NOT EXISTS trailer_url VARCHAR(500); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS video_url VARCHAR(500); + +-- Add indexes for video content +CREATE INDEX IF NOT EXISTS idx_movies_trailer_url ON movies(trailer_url) WHERE trailer_url IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_movies_video_url ON movies(video_url) WHERE video_url IS NOT NULL; + +-- Update existing movies with sample trailer URLs (using Big Buck Bunny as demo content) +UPDATE movies SET + trailer_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', + video_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4' +WHERE trailer_url IS NULL; + +-- Add some variety with different demo videos for different movies +UPDATE movies SET + trailer_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4', + video_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4' +WHERE id % 3 = 1; + +UPDATE movies SET + trailer_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4', + video_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4' +WHERE id % 3 = 2; + +UPDATE movies SET + trailer_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4', + video_url = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4' +WHERE id % 3 = 0; diff --git a/database/main/migrations/04_add_youtube_support.sql b/database/main/migrations/04_add_youtube_support.sql new file mode 100644 index 0000000..2a558e1 --- /dev/null +++ b/database/main/migrations/04_add_youtube_support.sql @@ -0,0 +1,39 @@ +-- Add YouTube-specific columns for better video handling +ALTER TABLE movies ADD COLUMN IF NOT EXISTS youtube_trailer_id VARCHAR(20); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS youtube_video_id VARCHAR(20); + +-- Add indexes for YouTube video IDs +CREATE INDEX IF NOT EXISTS idx_movies_youtube_trailer_id ON movies(youtube_trailer_id) WHERE youtube_trailer_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_movies_youtube_video_id ON movies(youtube_video_id) WHERE youtube_video_id IS NOT NULL; + +-- Function to extract YouTube video ID from URL +CREATE OR REPLACE FUNCTION extract_youtube_id(url TEXT) +RETURNS TEXT AS $$ +BEGIN + -- Handle various YouTube URL formats + IF url ~ 'youtube\.com/watch\?v=([^&]+)' THEN + RETURN substring(url from 'youtube\.com/watch\?v=([^&]+)'); + ELSIF url ~ 'youtu\.be/([^?]+)' THEN + RETURN substring(url from 'youtu\.be/([^?]+)'); + ELSIF url ~ 'youtube\.com/embed/([^?]+)' THEN + RETURN substring(url from 'youtube\.com/embed/([^?]+)'); + ELSIF url ~ 'youtube\.com/v/([^?]+)' THEN + RETURN substring(url from 'youtube\.com/v/([^?]+)'); + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +-- Update existing records to extract YouTube IDs from URLs +UPDATE movies +SET youtube_trailer_id = extract_youtube_id(trailer_url) +WHERE trailer_url IS NOT NULL + AND (trailer_url LIKE '%youtube.com%' OR trailer_url LIKE '%youtu.be%') + AND youtube_trailer_id IS NULL; + +UPDATE movies +SET youtube_video_id = extract_youtube_id(video_url) +WHERE video_url IS NOT NULL + AND (video_url LIKE '%youtube.com%' OR video_url LIKE '%youtu.be%') + AND youtube_video_id IS NULL; diff --git a/database/main/migrations/05_add_trailer_urls_simple.sql b/database/main/migrations/05_add_trailer_urls_simple.sql new file mode 100644 index 0000000..cf6944a --- /dev/null +++ b/database/main/migrations/05_add_trailer_urls_simple.sql @@ -0,0 +1,21 @@ +-- Simple migration to add trailer URLs to existing movies table +ALTER TABLE movies ADD COLUMN IF NOT EXISTS trailer_url VARCHAR(500); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS video_url VARCHAR(500); + +-- Update existing movies with YouTube trailer URLs +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=UaVTIH8mujA' WHERE title = 'The Godfather'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=vKQi3bBA1y8' WHERE title = 'The Matrix'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=EXeTwQWrcwY' WHERE title = 'The Dark Knight'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=YoHD9XEInc0' WHERE title = 'Inception'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=s7EdQ4FqbhY' WHERE title = 'Pulp Fiction'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=qtRKdVHc-cE' WHERE title = 'Fight Club'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=6hB3S9bIaco' WHERE title = 'The Shawshank Redemption'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=R2_Mn-qRKjA' WHERE title = 'Goodfellas'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=zSWdZVtXT7E' WHERE title = 'Interstellar'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=gCcx85zbxz4' WHERE title = 'Blade Runner 2049'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=n9xhJrPXop4' WHERE title = 'Dune'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=tFMo3UJ4B4g' WHERE title = 'Arrival'; +UPDATE movies SET trailer_url = 'https://www.youtube.com/watch?v=XYGzRB4Pnq8' WHERE title = 'Ex Machina'; + +-- Set video_url same as trailer_url for now +UPDATE movies SET video_url = trailer_url WHERE trailer_url IS NOT NULL; diff --git a/database/main/migrations/06_add_movie_metadata.sql b/database/main/migrations/06_add_movie_metadata.sql new file mode 100644 index 0000000..42e3cab --- /dev/null +++ b/database/main/migrations/06_add_movie_metadata.sql @@ -0,0 +1,96 @@ +-- Add comprehensive movie metadata columns +ALTER TABLE movies ADD COLUMN IF NOT EXISTS genres TEXT[]; +ALTER TABLE movies ADD COLUMN IF NOT EXISTS director VARCHAR(255); +ALTER TABLE movies ADD COLUMN IF NOT EXISTS cast TEXT[]; +ALTER TABLE movies ADD COLUMN IF NOT EXISTS duration INTEGER; -- in minutes + +-- Create indexes for better search performance +CREATE INDEX IF NOT EXISTS idx_movies_genres ON movies USING gin(genres); +CREATE INDEX IF NOT EXISTS idx_movies_director ON movies(director); +CREATE INDEX IF NOT EXISTS idx_movies_cast ON movies USING gin(cast); +CREATE INDEX IF NOT EXISTS idx_movies_duration ON movies(duration); + +-- Update existing movies with some basic metadata +UPDATE movies SET + genres = ARRAY['Crime', 'Drama'], + director = 'Francis Ford Coppola', + cast = ARRAY['Marlon Brando', 'Al Pacino', 'James Caan', 'Robert Duvall'], + duration = 175 +WHERE title = 'The Godfather'; + +UPDATE movies SET + genres = ARRAY['Action', 'Sci-Fi'], + director = 'The Wachowskis', + cast = ARRAY['Keanu Reeves', 'Laurence Fishburne', 'Carrie-Anne Moss', 'Hugo Weaving'], + duration = 136 +WHERE title = 'The Matrix'; + +UPDATE movies SET + genres = ARRAY['Action', 'Crime', 'Drama'], + director = 'Christopher Nolan', + cast = ARRAY['Christian Bale', 'Heath Ledger', 'Aaron Eckhart', 'Michael Caine'], + duration = 152 +WHERE title = 'The Dark Knight'; + +UPDATE movies SET + genres = ARRAY['Action', 'Sci-Fi', 'Thriller'], + director = 'Christopher Nolan', + cast = ARRAY['Leonardo DiCaprio', 'Marion Cotillard', 'Tom Hardy', 'Ellen Page'], + duration = 148 +WHERE title = 'Inception'; + +UPDATE movies SET + genres = ARRAY['Crime', 'Drama'], + director = 'Quentin Tarantino', + cast = ARRAY['John Travolta', 'Uma Thurman', 'Samuel L. Jackson', 'Bruce Willis'], + duration = 154 +WHERE title = 'Pulp Fiction'; + +UPDATE movies SET + genres = ARRAY['Drama'], + director = 'David Fincher', + cast = ARRAY['Brad Pitt', 'Edward Norton', 'Helena Bonham Carter', 'Meat Loaf'], + duration = 139 +WHERE title = 'Fight Club'; + +UPDATE movies SET + genres = ARRAY['Crime', 'Drama'], + director = 'Martin Scorsese', + cast = ARRAY['Robert De Niro', 'Ray Liotta', 'Joe Pesci', 'Lorraine Bracco'], + duration = 146 +WHERE title = 'Goodfellas'; + +UPDATE movies SET + genres = ARRAY['Adventure', 'Drama', 'Sci-Fi'], + director = 'Christopher Nolan', + cast = ARRAY['Matthew McConaughey', 'Anne Hathaway', 'Jessica Chastain', 'Michael Caine'], + duration = 169 +WHERE title = 'Interstellar'; + +UPDATE movies SET + genres = ARRAY['Action', 'Drama', 'Sci-Fi'], + director = 'Denis Villeneuve', + cast = ARRAY['Ryan Gosling', 'Harrison Ford', 'Ana de Armas', 'Jared Leto'], + duration = 164 +WHERE title = 'Blade Runner 2049'; + +UPDATE movies SET + genres = ARRAY['Adventure', 'Drama', 'Sci-Fi'], + director = 'Denis Villeneuve', + cast = ARRAY['TimothΓ©e Chalamet', 'Rebecca Ferguson', 'Oscar Isaac', 'Josh Brolin'], + duration = 155 +WHERE title = 'Dune'; + +UPDATE movies SET + genres = ARRAY['Drama', 'Sci-Fi'], + director = 'Denis Villeneuve', + cast = ARRAY['Amy Adams', 'Jeremy Renner', 'Forest Whitaker', 'Michael Stuhlbarg'], + duration = 116 +WHERE title = 'Arrival'; + +UPDATE movies SET + genres = ARRAY['Drama', 'Sci-Fi', 'Thriller'], + director = 'Alex Garland', + cast = ARRAY['Domhnall Gleeson', 'Alicia Vikander', 'Oscar Isaac', 'Sonoya Mizuno'], + duration = 108 +WHERE title = 'Ex Machina'; diff --git a/database/main/migrations/07_fix_cast_column.sql b/database/main/migrations/07_fix_cast_column.sql new file mode 100644 index 0000000..c91cc53 --- /dev/null +++ b/database/main/migrations/07_fix_cast_column.sql @@ -0,0 +1,6 @@ +-- Fix the cast column name issue (cast is a reserved word in PostgreSQL) +ALTER TABLE movies RENAME COLUMN cast TO movie_cast; + +-- Update the index +DROP INDEX IF EXISTS idx_movies_cast; +CREATE INDEX IF NOT EXISTS idx_movies_movie_cast ON movies USING gin(movie_cast); diff --git a/database/main/seeds/01_seed_trending.sql b/database/main/seeds/01_seed_trending.sql deleted file mode 100644 index 41a4801..0000000 --- a/database/main/seeds/01_seed_trending.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Insert trending movies -INSERT INTO movies ( - title, - description, - release_year, - rating, - image_url - ) -VALUES ( - 'The Matrix', - 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.', - 1999, - 8.7, - 'https://image.tmdb.org/t/p/w500/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg' - ), - ( - 'Inception', - 'A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.', - 2010, - 8.8, - 'https://image.tmdb.org/t/p/w500/9gk7adHYeDvHkCSEqAvQNLV5Uge.jpg' - ), - ( - 'Interstellar', - 'A team of explorers travel through a wormhole in space in an attempt to ensure humanity''s survival.', - 2014, - 8.6, - 'https://image.tmdb.org/t/p/w500/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg' - ); \ No newline at end of file diff --git a/database/main/seeds/02_seed_popular.sql b/database/main/seeds/02_seed_popular.sql deleted file mode 100644 index 96e85a7..0000000 --- a/database/main/seeds/02_seed_popular.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Insert popular movies -INSERT INTO movies ( - title, - description, - release_year, - rating, - image_url - ) -VALUES ( - 'The Dark Knight', - 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.', - 2008, - 9.0, - 'https://image.tmdb.org/t/p/w500/qJ2tW6WMUDux911r6m7haRef0WH.jpg' - ), - ( - 'Pulp Fiction', - 'The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', - 1994, - 8.9, - 'https://image.tmdb.org/t/p/w500/d5iIlFn5s0ImszYzBPb8JPIfbXD.jpg' - ), - ( - 'Fight Club', - 'An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.', - 1999, - 8.8, - 'https://image.tmdb.org/t/p/w500/a26cQPRhJPX6GbWfQbvZdrrp9j9.jpg' - ); \ No newline at end of file diff --git a/database/main/seeds/03_seed_classics.sql b/database/main/seeds/03_seed_classics.sql deleted file mode 100644 index af69548..0000000 --- a/database/main/seeds/03_seed_classics.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Insert classic movies -INSERT INTO movies ( - title, - description, - release_year, - rating, - image_url - ) -VALUES ( - 'Goodfellas', - 'The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen Hill and his mob partners Jimmy Conway and Tommy DeVito in the Italian-American crime syndicate.', - 1990, - 8.7, - 'https://image.tmdb.org/t/p/w500/aKuFiU82s5ISJpGZp7YkIr3kCUd.jpg' - ), - ( - 'The Godfather', - 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', - 1972, - 9.2, - 'https://image.tmdb.org/t/p/w500/rPdtLWNsZmAtoZl9PK7S2wE3qiS.jpg' - ), - ( - 'Blade Runner 2049', - 'A young blade runner''s discovery of a long-buried secret leads him to track down former blade runner Rick Deckard, who''s been missing for thirty years.', - 2017, - 8.0, - 'https://image.tmdb.org/t/p/w500/aMpyrCizvSdc0UIMblJ1srVgAEF.jpg' - ); \ No newline at end of file diff --git a/database/main/seeds/04_seed_scifi.sql b/database/main/seeds/04_seed_scifi.sql deleted file mode 100644 index 329d1e1..0000000 --- a/database/main/seeds/04_seed_scifi.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Insert sci-fi movies -INSERT INTO movies ( - title, - description, - release_year, - rating, - image_url - ) -VALUES ( - 'Dune', - 'Feature adaptation of Frank Herbert''s science fiction novel about the son of a noble family entrusted with the protection of the most valuable asset and most vital element in the galaxy.', - 2021, - 8.0, - 'https://image.tmdb.org/t/p/w500/d5NXSklXo0qyIYkgV94XAgMIckC.jpg' - ), - ( - 'Arrival', - 'A linguist works with the military to communicate with alien lifeforms after twelve mysterious spacecraft appear around the world.', - 2016, - 7.9, - 'https://image.tmdb.org/t/p/w500/x2FJsf1ElAgr63Y3PNPtJrcmpoe.jpg' - ), - ( - 'Ex Machina', - 'A young programmer is selected to participate in a ground-breaking experiment in synthetic intelligence by evaluating the human qualities of a highly advanced humanoid A.I.', - 2014, - 7.7, - 'https://image.tmdb.org/t/p/w500/pmAv14TPE2vKMIRrVeCd1Ll7K94.jpg' - ); \ No newline at end of file diff --git a/database/main/seeds/movies_complete.sql b/database/main/seeds/movies_complete.sql new file mode 100644 index 0000000..f780d8b --- /dev/null +++ b/database/main/seeds/movies_complete.sql @@ -0,0 +1,408 @@ +-- Complete movie database with all categories and metadata +-- This replaces all the scattered seed files with one comprehensive dataset + +TRUNCATE TABLE movies; + +INSERT INTO movies ( + title, + description, + release_year, + rating, + image_url, + trailer_url, + youtube_trailer_id, + video_url, + genres, + director, + movie_cast, + duration, + content_type, + keywords, + categories +) VALUES + +-- TRENDING MOVIES +( + 'The Matrix', + 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.', + 1999, + 8.7, + 'https://image.tmdb.org/t/p/w500/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + 'vKQi3bBA1y8', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + ARRAY['Action', 'Sci-Fi'], + 'Lana Wachowski, Lilly Wachowski', + ARRAY['Keanu Reeves', 'Laurence Fishburne', 'Carrie-Anne Moss', 'Hugo Weaving'], + 136, + 'movie', + ARRAY['matrix', 'reality', 'simulation', 'neo', 'morpheus'], + ARRAY['trending', 'scifi'] +), +( + 'Inception', + 'A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.', + 2010, + 8.8, + 'https://image.tmdb.org/t/p/w500/9gk7adHYeDvHkCSEqAvQNLV5Uge.jpg', + 'https://www.youtube.com/watch?v=YoHD9XEInc0', + 'YoHD9XEInc0', + 'https://www.youtube.com/watch?v=YoHD9XEInc0', + ARRAY['Action', 'Sci-Fi', 'Thriller'], + 'Christopher Nolan', + ARRAY['Leonardo DiCaprio', 'Marion Cotillard', 'Tom Hardy', 'Ellen Page'], + 148, + 'movie', + ARRAY['dreams', 'inception', 'subconscious', 'heist'], + ARRAY['trending', 'scifi'] +), +( + 'The Dark Knight', + 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.', + 2008, + 9.0, + 'https://image.tmdb.org/t/p/w500/qJ2tW6WMUDux911r6m7haRef0WH.jpg', + 'https://www.youtube.com/watch?v=EXeTwQWrcwY', + 'EXeTwQWrcwY', + 'https://www.youtube.com/watch?v=EXeTwQWrcwY', + ARRAY['Action', 'Crime', 'Drama'], + 'Christopher Nolan', + ARRAY['Christian Bale', 'Heath Ledger', 'Aaron Eckhart', 'Michael Caine'], + 152, + 'movie', + ARRAY['batman', 'joker', 'gotham', 'superhero'], + ARRAY['trending', 'popular'] +), +( + 'Interstellar', + 'A team of explorers travel through a wormhole in space in an attempt to ensure humanity''s survival.', + 2014, + 8.6, + 'https://image.tmdb.org/t/p/w500/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg', + 'https://www.youtube.com/watch?v=zSWdZVtXT7E', + 'zSWdZVtXT7E', + 'https://www.youtube.com/watch?v=zSWdZVtXT7E', + ARRAY['Sci-Fi', 'Drama', 'Adventure'], + 'Christopher Nolan', + ARRAY['Matthew McConaughey', 'Anne Hathaway', 'Jessica Chastain', 'Michael Caine'], + 169, + 'movie', + ARRAY['space', 'wormhole', 'time', 'survival'], + ARRAY['trending', 'scifi'] +), + +-- POPULAR MOVIES +( + 'Pulp Fiction', + 'The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', + 1994, + 8.9, + 'https://image.tmdb.org/t/p/w500/d5iIlFn5s0ImszYzBPb8JPIfbXD.jpg', + 'https://www.youtube.com/watch?v=s7EdQ4FqbhY', + 's7EdQ4FqbhY', + 'https://www.youtube.com/watch?v=s7EdQ4FqbhY', + ARRAY['Crime', 'Drama'], + 'Quentin Tarantino', + ARRAY['John Travolta', 'Uma Thurman', 'Samuel L. Jackson', 'Bruce Willis'], + 154, + 'movie', + ARRAY['crime', 'violence', 'redemption', 'tarantino'], + ARRAY['popular', 'classics'] +), +( + 'Fight Club', + 'An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.', + 1999, + 8.8, + 'https://image.tmdb.org/t/p/w500/a26cQPRhJPX6GbWfQbvZdrrp9j9.jpg', + 'https://www.youtube.com/watch?v=qtRKdVHc-cE', + 'qtRKdVHc-cE', + 'https://www.youtube.com/watch?v=qtRKdVHc-cE', + ARRAY['Drama', 'Thriller'], + 'David Fincher', + ARRAY['Brad Pitt', 'Edward Norton', 'Helena Bonham Carter'], + 139, + 'movie', + ARRAY['fight', 'club', 'society', 'rebellion'], + ARRAY['popular'] +), +( + 'Goodfellas', + 'The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen Hill and his mob partners Jimmy Conway and Tommy DeVito in the Italian-American crime syndicate.', + 1990, + 8.7, + 'https://image.tmdb.org/t/p/w500/aKuFiU82s5ISJpGZp7YkIr3kCUd.jpg', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + 'vKQi3bBA1y8', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + ARRAY['Crime', 'Drama'], + 'Martin Scorsese', + ARRAY['Robert De Niro', 'Ray Liotta', 'Joe Pesci', 'Lorraine Bracco'], + 146, + 'movie', + ARRAY['mob', 'mafia', 'crime', 'scorsese'], + ARRAY['popular', 'classics'] +), +( + 'The Godfather', + 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', + 1972, + 9.2, + 'https://image.tmdb.org/t/p/w500/rPdtLWNsZmAtoZl9PK7S2wE3qiS.jpg', + 'https://www.youtube.com/watch?v=UaVTIH8mujA', + 'UaVTIH8mujA', + 'https://www.youtube.com/watch?v=UaVTIH8mujA', + ARRAY['Crime', 'Drama'], + 'Francis Ford Coppola', + ARRAY['Marlon Brando', 'Al Pacino', 'James Caan', 'Diane Keaton'], + 175, + 'movie', + ARRAY['godfather', 'mafia', 'family', 'power'], + ARRAY['popular', 'classics'] +), + +-- SCI-FI MOVIES +( + 'Blade Runner 2049', + 'A young blade runner''s discovery of a long-buried secret leads him to track down former blade runner Rick Deckard, who''s been missing for thirty years.', + 2017, + 8.0, + 'https://image.tmdb.org/t/p/w500/aMpyrCizvSdc0UIMblJ1srVgAEF.jpg', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + 'vKQi3bBA1y8', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + ARRAY['Sci-Fi', 'Thriller'], + 'Denis Villeneuve', + ARRAY['Ryan Gosling', 'Harrison Ford', 'Ana de Armas', 'Jared Leto'], + 164, + 'movie', + ARRAY['blade runner', 'replicant', 'future', 'android'], + ARRAY['scifi'] +), +( + 'Dune', + 'Feature adaptation of Frank Herbert''s science fiction novel about the son of a noble family entrusted with the protection of the most valuable asset and most vital element in the galaxy.', + 2021, + 8.0, + 'https://image.tmdb.org/t/p/w500/d5NXSklXo0qyIYkgV94XAgMIckC.jpg', + 'https://www.youtube.com/watch?v=n9xhJrPXop4', + 'n9xhJrPXop4', + 'https://www.youtube.com/watch?v=n9xhJrPXop4', + ARRAY['Sci-Fi', 'Adventure', 'Drama'], + 'Denis Villeneuve', + ARRAY['TimothΓ©e Chalamet', 'Rebecca Ferguson', 'Oscar Isaac', 'Josh Brolin'], + 155, + 'movie', + ARRAY['dune', 'spice', 'desert', 'prophecy'], + ARRAY['scifi', 'modernBlockbusters'] +), +( + 'Arrival', + 'A linguist works with the military to communicate with alien lifeforms after twelve mysterious spacecraft appear around the world.', + 2016, + 7.9, + 'https://image.tmdb.org/t/p/w500/x2FJsf1ElAgr63Y3PNPtJrcmpoe.jpg', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + 'vKQi3bBA1y8', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + ARRAY['Sci-Fi', 'Adventure', 'Drama'], + 'Denis Villeneuve', + ARRAY['Amy Adams', 'Jeremy Renner', 'Forest Whitaker'], + 116, + 'movie', + ARRAY['aliens', 'language', 'communication', 'time'], + ARRAY['scifi'] +), +( + 'Ex Machina', + 'A young programmer is selected to participate in a ground-breaking experiment in synthetic intelligence by evaluating the human qualities of a highly advanced humanoid A.I.', + 2014, + 7.7, + 'https://image.tmdb.org/t/p/w500/pmAv14TPE2vKMIRrVeCd1Ll7K94.jpg', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + 'vKQi3bBA1y8', + 'https://www.youtube.com/watch?v=vKQi3bBA1y8', + ARRAY['Sci-Fi', 'Thriller'], + 'Alex Garland', + ARRAY['Domhnall Gleeson', 'Alicia Vikander', 'Oscar Isaac'], + 108, + 'movie', + ARRAY['ai', 'artificial intelligence', 'robot', 'turing test'], + ARRAY['scifi'] +), + +-- AWARD WINNERS +( + 'Parasite', + 'A poor family schemes to become employed by a wealthy family and infiltrate their household by posing as unrelated, highly qualified individuals.', + 2019, + 8.5, + 'https://image.tmdb.org/t/p/w500/7IiTTgloJzvGI1TAYymCfbfl3vT.jpg', + 'https://www.youtube.com/watch?v=5xH0HfJHsaY', + '5xH0HfJHsaY', + 'https://www.youtube.com/watch?v=5xH0HfJHsaY', + ARRAY['Thriller', 'Drama', 'Comedy'], + 'Bong Joon-ho', + ARRAY['Song Kang-ho', 'Lee Sun-kyun', 'Cho Yeo-jeong', 'Choi Woo-shik', 'Park So-dam'], + 132, + 'movie', + ARRAY['class', 'society', 'family', 'deception'], + ARRAY['awardWinners'] +), +( + 'Green Book', + 'A working-class Italian-American bouncer becomes the driver of an African-American classical pianist on a tour of venues through the 1960s American South.', + 2018, + 8.2, + 'https://image.tmdb.org/t/p/w500/7BsvSuDQuoqhWmU2fL7W2GOcZHU.jpg', + 'https://www.youtube.com/watch?v=QkZxoko_HC0', + 'QkZxoko_HC0', + 'https://www.youtube.com/watch?v=QkZxoko_HC0', + ARRAY['Biography', 'Comedy', 'Drama'], + 'Peter Farrelly', + ARRAY['Viggo Mortensen', 'Mahershala Ali', 'Linda Cardellini', 'Sebastian Maniscalco'], + 130, + 'movie', + ARRAY['racism', 'friendship', 'music', 'road trip'], + ARRAY['awardWinners'] +), +( + 'Moonlight', + 'A young African-American man grapples with his identity and sexuality while experiencing the everyday struggles of childhood, adolescence, and burgeoning adulthood.', + 2016, + 7.4, + 'https://m.media-amazon.com/images/M/MV5BNzQxNTIyODAxMV5BMl5BanBnXkFtZTgwNzQyMDA3OTE@._V1_SX300.jpg', + 'https://www.youtube.com/watch?v=9NJj12tJzqc', + '9NJj12tJzqc', + 'https://www.youtube.com/watch?v=9NJj12tJzqc', + ARRAY['Drama'], + 'Barry Jenkins', + ARRAY['Trevante Rhodes', 'AndrΓ© Holland', 'Janelle MonΓ‘e', 'Ashton Sanders', 'Jharrel Jerome'], + 111, + 'movie', + ARRAY['identity', 'sexuality', 'coming of age', 'african american'], + ARRAY['awardWinners'] +), +( + 'The Shape of Water', + 'At a top secret research facility in the 1960s, a lonely janitor forms a unique relationship with an amphibious creature that is being held in captivity.', + 2017, + 7.3, + 'https://image.tmdb.org/t/p/w500/k4FwHlMhuRR5BISY2Gm2QZHlH5Q.jpg', + 'https://www.youtube.com/watch?v=XFYWazblaUA', + 'XFYWazblaUA', + 'https://www.youtube.com/watch?v=XFYWazblaUA', + ARRAY['Drama', 'Fantasy', 'Romance'], + 'Guillermo del Toro', + ARRAY['Sally Hawkins', 'Michael Shannon', 'Richard Jenkins', 'Doug Jones', 'Michael Stuhlbarg'], + 123, + 'movie', + ARRAY['love', 'creature', 'fantasy', 'romance'], + ARRAY['awardWinners'] +), + +-- MODERN BLOCKBUSTERS +( + 'Avatar: The Way of Water', + 'Jake Sully lives with his newfound family formed on the extrasolar moon Pandora. Once a familiar threat returns to finish what was previously started, Jake must work with Neytiri and the army of the Navi race to protect their home.', + 2022, + 7.6, + 'https://image.tmdb.org/t/p/w500/t6HIqrRAclMCA60NsSmeqe9RmNV.jpg', + 'https://www.youtube.com/watch?v=d9MyW72ELq0', + 'd9MyW72ELq0', + 'https://www.youtube.com/watch?v=d9MyW72ELq0', + ARRAY['Action', 'Adventure', 'Sci-Fi'], + 'James Cameron', + ARRAY['Sam Worthington', 'Zoe Saldana', 'Sigourney Weaver', 'Stephen Lang', 'Kate Winslet'], + 192, + 'movie', + ARRAY['avatar', 'pandora', 'navi', 'ocean'], + ARRAY['modernBlockbusters', 'scifi'] +), +( + 'Top Gun: Maverick', + 'After thirty years, Maverick is still pushing the envelope as a top naval aviator, but must confront ghosts of his past when he leads TOP GUNs elite graduates on a mission that demands the ultimate sacrifice from those chosen to fly it.', + 2022, + 8.3, + 'https://image.tmdb.org/t/p/w500/62HCnUTziyWcpDaBO2i1DX17ljH.jpg', + 'https://www.youtube.com/watch?v=qSqVVswa420', + 'qSqVVswa420', + 'https://www.youtube.com/watch?v=qSqVVswa420', + ARRAY['Action', 'Drama'], + 'Joseph Kosinski', + ARRAY['Tom Cruise', 'Miles Teller', 'Jennifer Connelly', 'Jon Hamm', 'Glen Powell'], + 130, + 'movie', + ARRAY['aviation', 'military', 'sequel', 'action'], + ARRAY['modernBlockbusters'] +), +( + 'Spider-Man: No Way Home', + 'With Spider-Mans identity now revealed, Peter asks Doctor Strange for help. When a spell goes wrong, dangerous foes from other worlds start to appear, forcing Peter to discover what it truly means to be Spider-Man.', + 2021, + 8.2, + 'https://image.tmdb.org/t/p/w500/1g0dhYtq4irTY1GPXvft6k4YLjm.jpg', + 'https://www.youtube.com/watch?v=JfVOs4VSpmA', + 'JfVOs4VSpmA', + 'https://www.youtube.com/watch?v=JfVOs4VSpmA', + ARRAY['Action', 'Adventure', 'Sci-Fi'], + 'Jon Watts', + ARRAY['Tom Holland', 'Zendaya', 'Benedict Cumberbatch', 'Jacob Batalon', 'Jon Favreau'], + 148, + 'movie', + ARRAY['spider-man', 'multiverse', 'superhero', 'marvel'], + ARRAY['modernBlockbusters', 'scifi'] +), +( + 'Avengers: Endgame', + 'After the devastating events of Infinity War, the Avengers assemble once more to reverse Thanos actions and restore balance to the universe.', + 2019, + 8.4, + 'https://image.tmdb.org/t/p/w500/or06FN3Dka5tukK1e9sl16pB3iy.jpg', + 'https://www.youtube.com/watch?v=TcMBFSGVi1c', + 'TcMBFSGVi1c', + 'https://www.youtube.com/watch?v=TcMBFSGVi1c', + ARRAY['Action', 'Adventure', 'Drama'], + 'Anthony Russo, Joe Russo', + ARRAY['Robert Downey Jr.', 'Chris Evans', 'Mark Ruffalo', 'Chris Hemsworth', 'Scarlett Johansson'], + 181, + 'movie', + ARRAY['avengers', 'marvel', 'superhero', 'finale'], + ARRAY['modernBlockbusters'] +), + +-- Additional movies to fill out categories to 5 each +( + 'The Shawshank Redemption', + 'Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.', + 1994, + 9.3, + 'https://image.tmdb.org/t/p/w500/q6y0Go1tsGEsmtFryDOJo3dEmqu.jpg', + 'https://www.youtube.com/watch?v=6hB3S9bIaco', + '6hB3S9bIaco', + 'https://www.youtube.com/watch?v=6hB3S9bIaco', + ARRAY['Drama'], + 'Frank Darabont', + ARRAY['Tim Robbins', 'Morgan Freeman', 'Bob Gunton', 'William Sadler'], + 142, + 'movie', + ARRAY['prison', 'hope', 'friendship', 'redemption'], + ARRAY['trending'] +), +( + 'Nomadland', + 'A woman in her sixties embarks on a journey through the western United States after losing everything in the Great Recession, living in a van and working seasonal jobs.', + 2020, + 7.3, + 'https://m.media-amazon.com/images/M/MV5BNDIzNDU0YzEtYzE5Ni00ZjlkLTk5ZjgtNjM3NWE4YzA3Nzk3XkEyXkFqcGdeQXVyMjUzOTY1NTc@._V1_SX300.jpg', + 'https://www.youtube.com/watch?v=6sxCFZ8_d84', + '6sxCFZ8_d84', + 'https://www.youtube.com/watch?v=6sxCFZ8_d84', + ARRAY['Drama'], + 'ChloΓ© Zhao', + ARRAY['Frances McDormand', 'David Strathairn', 'Linda May', 'Charlene Swankie'], + 107, + 'movie', + ARRAY['nomad', 'recession', 'journey', 'survival'], + ARRAY['awardWinners'] +); diff --git a/database/main/seeds/seed_movies.sql b/database/main/seeds/seed_movies.sql deleted file mode 100644 index 2cf98ee..0000000 --- a/database/main/seeds/seed_movies.sql +++ /dev/null @@ -1,94 +0,0 @@ --- Clear existing data -TRUNCATE TABLE movies; --- Insert sample movies -INSERT INTO movies ( - title, - description, - release_year, - rating, - image_url - ) -VALUES ( - 'The Matrix', - 'A computer hacker learns from mysterious rebels about the true nature of his reality and his role in the war against its controllers.', - 1999, - 8.7, - 'https://image.tmdb.org/t/p/w500/f89U3ADr1oiB1s9GkdPOEpXUk5H.jpg' - ), - ( - 'Inception', - 'A thief who steals corporate secrets through the use of dream-sharing technology is given the inverse task of planting an idea into the mind of a C.E.O.', - 2010, - 8.8, - 'https://image.tmdb.org/t/p/w500/9gk7adHYeDvHkCSEqAvQNLV5Uge.jpg' - ), - ( - 'Interstellar', - 'A team of explorers travel through a wormhole in space in an attempt to ensure humanity''s survival.', - 2014, - 8.6, - 'https://image.tmdb.org/t/p/w500/gEU2QniE6E77NI6lCU6MxlNBvIx.jpg' - ), - ( - 'The Dark Knight', - 'When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.', - 2008, - 9.0, - 'https://image.tmdb.org/t/p/w500/qJ2tW6WMUDux911r6m7haRef0WH.jpg' - ), - ( - 'Pulp Fiction', - 'The lives of two mob hitmen, a boxer, a gangster and his wife, and a pair of diner bandits intertwine in four tales of violence and redemption.', - 1994, - 8.9, - 'https://image.tmdb.org/t/p/w500/d5iIlFn5s0ImszYzBPb8JPIfbXD.jpg' - ), - ( - 'Fight Club', - 'An insomniac office worker and a devil-may-care soapmaker form an underground fight club that evolves into something much, much more.', - 1999, - 8.8, - 'https://image.tmdb.org/t/p/w500/a26cQPRhJPX6GbWfQbvZdrrp9j9.jpg' - ), - ( - 'Goodfellas', - 'The story of Henry Hill and his life in the mob, covering his relationship with his wife Karen Hill and his mob partners Jimmy Conway and Tommy DeVito in the Italian-American crime syndicate.', - 1990, - 8.7, - 'https://image.tmdb.org/t/p/w500/aKuFiU82s5ISJpGZp7YkIr3kCUd.jpg' - ), - ( - 'The Godfather', - 'The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.', - 1972, - 9.2, - 'https://image.tmdb.org/t/p/w500/rPdtLWNsZmAtoZl9PK7S2wE3qiS.jpg' - ), - ( - 'Blade Runner 2049', - 'A young blade runner''s discovery of a long-buried secret leads him to track down former blade runner Rick Deckard, who''s been missing for thirty years.', - 2017, - 8.0, - 'https://image.tmdb.org/t/p/w500/aMpyrCizvSdc0UIMblJ1srVgAEF.jpg' - ), - ( - 'Dune', - 'Feature adaptation of Frank Herbert''s science fiction novel about the son of a noble family entrusted with the protection of the most valuable asset and most vital element in the galaxy.', - 2021, - 8.0, - 'https://image.tmdb.org/t/p/w500/d5NXSklXo0qyIYkgV94XAgMIckC.jpg' - ), - ( - 'Arrival', - 'A linguist works with the military to communicate with alien lifeforms after twelve mysterious spacecraft appear around the world.', - 2016, - 7.9, - 'https://image.tmdb.org/t/p/w500/x2FJsf1ElAgr63Y3PNPtJrcmpoe.jpg' - ), - ( - 'Ex Machina', - 'A young programmer is selected to participate in a ground-breaking experiment in synthetic intelligence by evaluating the human qualities of a highly advanced humanoid A.I.', - 2014, - 7.7, - 'https://image.tmdb.org/t/p/w500/pmAv14TPE2vKMIRrVeCd1Ll7K94.jpg' - ); \ No newline at end of file diff --git a/frontend/public/images/profile-avatar.jpg b/frontend/public/images/profile-avatar.jpg new file mode 100644 index 0000000..e567011 Binary files /dev/null and b/frontend/public/images/profile-avatar.jpg differ diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0f57099..550549f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,25 +3,51 @@ import { BrowserRouter as Router, Routes, Route, createBrowserRouter, RouterProv import Navbar from './components/Navbar' import Hero from './components/Hero' import MovieRow from './components/MovieRow' +import SearchResults from './components/SearchResults' +import MovieModal from './components/MovieModal' import { fetchMovies } from './services/api' function Home() { const [movies, setMovies] = useState({ trending: [], popular: [], - scifi: [] + scifi: [], + awardWinners: [], + modernBlockbusters: [] }); + const [searchResults, setSearchResults] = useState([]) + const [isSearchActive, setIsSearchActive] = useState(false) + const [searchQuery, setSearchQuery] = useState('') + const [selectedMovie, setSelectedMovie] = useState(null) + const [showMovieModal, setShowMovieModal] = useState(false) useEffect(() => { const loadMovies = async () => { try { const allMovies = await fetchMovies(); - // Organize movies into categories based on rating - const trending = allMovies.slice(0, 4); - const popular = allMovies.slice(4, 8); - const scifi = allMovies.slice(8, 12); - setMovies({ trending, popular, scifi }); + // Organize movies into categories using the new categories column + const trending = allMovies.filter(movie => + movie.categories && movie.categories.includes('trending') + ).slice(0, 5); + + const popular = allMovies.filter(movie => + movie.categories && movie.categories.includes('popular') + ).slice(0, 5); + + const scifi = allMovies.filter(movie => + movie.categories && movie.categories.includes('scifi') + ).slice(0, 5); + + const awardWinners = allMovies.filter(movie => + movie.categories && movie.categories.includes('awardWinners') + ).slice(0, 5); + + const modernBlockbusters = allMovies.filter(movie => + movie.categories && movie.categories.includes('modernBlockbusters') + ).slice(0, 5); + + setMovies({ trending, popular, scifi, awardWinners, modernBlockbusters }); } catch (error) { console.error('Error loading movies:', error); } @@ -30,23 +56,121 @@ function Home() { loadMovies(); }, []); + const handleSearchResults = (results) => { + setSearchResults(results) + setIsSearchActive(results.length > 0) + } + + const handleSearchToggle = (isActive) => { + setIsSearchActive(isActive) + if (!isActive) { + setSearchResults([]) + setSearchQuery('') + } + } + + const handleClearSearch = () => { + setSearchResults([]) + setIsSearchActive(false) + setSearchQuery('') + } + + const handleMovieClick = (movie) => { + setSelectedMovie(movie) + setShowMovieModal(true) + } + + const handleCloseModal = () => { + setShowMovieModal(false) + setSelectedMovie(null) + } + return (
- - -
- - - -
+ + + {isSearchActive && searchResults.length > 0 ? ( +
+ +
+ ) : ( + <> + +
+ + + + + +
+ + )} + + {/* Movie Modal */} +
) } +// Import test components +import VideoPlayerDebug from './components/VideoPlayerDebug' +import QuickTest from './components/QuickTest' +import AudioTest from './components/AudioTest' +import CategoryTest from './components/CategoryTest' + const router = createBrowserRouter([ { path: "/", element: , + }, + { + path: "/debug", + element: , + }, + { + path: "/test", + element: , + }, + { + path: "/audio", + element: , + }, + { + path: "/categories", + element: , } ], { future: { @@ -59,4 +183,4 @@ function App() { return } -export default App \ No newline at end of file +export default App diff --git a/frontend/src/components/AudioTest.jsx b/frontend/src/components/AudioTest.jsx new file mode 100644 index 0000000..7c05f9a --- /dev/null +++ b/frontend/src/components/AudioTest.jsx @@ -0,0 +1,91 @@ +import React, { useState } from 'react' +import YouTubePlayer from './YouTubePlayer' + +function AudioTest() { + const [showPlayer, setShowPlayer] = useState(false) + + const testMovie = { + id: 1, + title: "The Godfather - Audio Test", + description: "Testing YouTube audio functionality", + release_year: 1972, + rating: 9.2, + image_url: "https://image.tmdb.org/t/p/w500/rPdtLWNsZmAtoZl9PK7S2wE3qiS.jpg", + trailer_url: "https://www.youtube.com/watch?v=UaVTIH8mujA", + video_url: "https://www.youtube.com/watch?v=UaVTIH8mujA" + } + + return ( +
+
+

YouTube Audio Test

+ +
+

Audio Features Enabled

+
+
+

βœ… Fixed Issues

+
    +
  • β€’ Sound enabled (mute: 0)
  • +
  • β€’ YouTube controls visible (controls: 1)
  • +
  • β€’ Proper iframe dimensions
  • +
  • β€’ No control cropping
  • +
  • β€’ Volume controls accessible
  • +
  • β€’ Fullscreen enabled
  • +
  • β€’ Keyboard controls enabled
  • +
+
+
+

🎡 Audio Features

+
    +
  • β€’ Volume slider in YouTube controls
  • +
  • β€’ Mute/unmute button
  • +
  • β€’ Closed captions available
  • +
  • β€’ Full audio quality
  • +
  • β€’ Keyboard volume controls (↑↓)
  • +
  • β€’ Right-click context menu
  • +
  • β€’ Audio settings in YouTube menu
  • +
+
+
+
+ +
+

⚠️ Browser Autoplay Policy

+

+ Some browsers may still start videos muted due to autoplay policies. + If the video starts without sound, click the volume button in the YouTube controls to unmute. +

+
+ +
+

Testing with: The Godfather (1972) Official Trailer

+

This trailer has clear dialogue and music to test audio quality.

+
+ + + +
+

Expected behavior:

+

1. Video opens with YouTube controls visible at bottom

+

2. Volume controls are accessible and functional

+

3. Audio plays clearly (may start muted due to browser policy)

+

4. All YouTube controls work including fullscreen

+
+
+ + setShowPlayer(false)} + /> +
+ ) +} + +export default AudioTest diff --git a/frontend/src/components/CategoryTest.jsx b/frontend/src/components/CategoryTest.jsx new file mode 100644 index 0000000..44e69ef --- /dev/null +++ b/frontend/src/components/CategoryTest.jsx @@ -0,0 +1,169 @@ +import React, { useState, useEffect } from 'react' +import { fetchMovies } from '../services/api' +import MovieRow from './MovieRow' + +function CategoryTest() { + const [allMovies, setAllMovies] = useState([]) + const [categories, setCategories] = useState({}) + + useEffect(() => { + const loadMovies = async () => { + try { + const movies = await fetchMovies() + setAllMovies(movies) + + // Organize movies into categories + const trending = movies.filter(movie => movie.rating >= 8.5).slice(0, 4) + const popular = movies.filter(movie => movie.rating >= 8.0 && movie.rating < 8.5).slice(0, 4) + const scifi = movies.filter(movie => + movie.genres && movie.genres.some(genre => + genre.toLowerCase().includes('sci-fi') || + genre.toLowerCase().includes('science fiction') + ) + ).slice(0, 4) + + const awardWinners = movies.filter(movie => + ['Parasite', 'Green Book', 'Moonlight', 'The Shape of Water'].includes(movie.title) + ).slice(0, 4) + + const modernBlockbusters = movies.filter(movie => + ['Avatar: The Way of Water', 'Top Gun: Maverick', 'Spider-Man: No Way Home', 'Avengers: Endgame'].includes(movie.title) + ).slice(0, 4) + + setCategories({ trending, popular, scifi, awardWinners, modernBlockbusters }) + } catch (error) { + console.error('Error loading movies:', error) + } + } + + loadMovies() + }, []) + + return ( +
+
+

Movie Categories Test

+ + {/* Category Statistics */} +
+

Category Statistics

+
+
+
{categories.trending?.length || 0}
+
Trending Now
+
+
+
{categories.popular?.length || 0}
+
Popular
+
+
+
{categories.scifi?.length || 0}
+
Sci-Fi
+
+
+
{categories.awardWinners?.length || 0}
+
Award Winners
+
+
+
{categories.modernBlockbusters?.length || 0}
+
Modern Blockbusters
+
+
+
+
{allMovies.length}
+
Total Movies
+
+
+ + {/* Movie Categories */} +
+ {categories.trending && categories.trending.length > 0 && ( +
+

Trending Now (Rating β‰₯ 8.5)

+
+ {categories.trending.map(movie => ( +
+ {movie.title} +

{movie.title}

+

{movie.release_year} β€’ ⭐ {movie.rating}

+

{movie.director}

+
+ ))} +
+
+ )} + + {categories.awardWinners && categories.awardWinners.length > 0 && ( +
+

Award Winners (Oscar Best Picture)

+
+ {categories.awardWinners.map(movie => ( +
+ {movie.title} +

{movie.title}

+

{movie.release_year} β€’ ⭐ {movie.rating}

+

{movie.director}

+
+ {movie.genres && movie.genres.map(genre => ( + + {genre} + + ))} +
+
+ ))} +
+
+ )} + + {categories.modernBlockbusters && categories.modernBlockbusters.length > 0 && ( +
+

Modern Blockbusters (2019-2022)

+
+ {categories.modernBlockbusters.map(movie => ( +
+ {movie.title} +

{movie.title}

+

{movie.release_year} β€’ ⭐ {movie.rating}

+

{movie.director}

+

{movie.duration} min

+
+ {movie.genres && movie.genres.map(genre => ( + + {genre} + + ))} +
+
+ ))} +
+
+ )} + + {categories.scifi && categories.scifi.length > 0 && ( +
+

Sci-Fi & Fantasy

+
+ {categories.scifi.map(movie => ( +
+ {movie.title} +

{movie.title}

+

{movie.release_year} β€’ ⭐ {movie.rating}

+

{movie.director}

+
+ ))} +
+
+ )} +
+ +
+

Visit the main app to see these categories in Netflix-style rows!

+

All movies include full metadata: genres, director, cast, duration, and YouTube trailers.

+
+
+
+ ) +} + +export default CategoryTest diff --git a/frontend/src/components/FilterPanel.jsx b/frontend/src/components/FilterPanel.jsx new file mode 100644 index 0000000..5f42902 --- /dev/null +++ b/frontend/src/components/FilterPanel.jsx @@ -0,0 +1,276 @@ +import React from 'react' + +function FilterPanel({ filters, onFiltersChange, onClear, isVisible, onClose }) { + const genreOptions = [ + 'Action', 'Adventure', 'Animation', 'Comedy', 'Crime', 'Documentary', + 'Drama', 'Family', 'Fantasy', 'Horror', 'Music', 'Mystery', 'Romance', + 'Science Fiction', 'Thriller', 'War', 'Western' + ] + + const handleGenreToggle = (genre) => { + const newGenres = filters.genres.includes(genre) + ? filters.genres.filter(g => g !== genre) + : [...filters.genres, genre] + + onFiltersChange({ + ...filters, + genres: newGenres + }) + } + + const handleYearChange = (type, value) => { + onFiltersChange({ + ...filters, + yearRange: { + ...filters.yearRange, + [type]: parseInt(value) + } + }) + } + + const handleRatingChange = (type, value) => { + onFiltersChange({ + ...filters, + ratingRange: { + ...filters.ratingRange, + [type]: parseFloat(value) + } + }) + } + + const handleDurationChange = (type, value) => { + onFiltersChange({ + ...filters, + durationRange: { + ...filters.durationRange, + [type]: parseInt(value) + } + }) + } + + const getActiveFiltersCount = () => { + let count = 0 + if (filters.genres.length > 0) count++ + if (filters.yearRange.min > 1900 || filters.yearRange.max < new Date().getFullYear()) count++ + if (filters.ratingRange.min > 0 || filters.ratingRange.max < 10) count++ + if (filters.durationRange.min > 0 || filters.durationRange.max < 300) count++ + return count + } + + if (!isVisible) return null + + return ( +
+ {/* Header */} +
+
+

Filters

+ {getActiveFiltersCount() > 0 && ( + + {getActiveFiltersCount()} + + )} +
+
+ + +
+
+ +
+ {/* Genres */} +
+ +
+ {genreOptions.map(genre => ( + + ))} +
+
+ + {/* Year Range */} +
+ +
+
+
+ + handleYearChange('min', e.target.value)} + className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-red-500 focus:outline-none transition-colors" + /> +
+
+ + handleYearChange('max', e.target.value)} + className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-red-500 focus:outline-none transition-colors" + /> +
+
+ handleYearChange('min', e.target.value)} + className="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + /> +
+
+ + {/* Rating Range */} +
+ +
+
+
+ + handleRatingChange('min', e.target.value)} + className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-red-500 focus:outline-none transition-colors" + /> +
+
+ + handleRatingChange('max', e.target.value)} + className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-red-500 focus:outline-none transition-colors" + /> +
+
+
+ 0 + handleRatingChange('min', e.target.value)} + className="flex-1 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + /> + handleRatingChange('max', e.target.value)} + className="flex-1 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + /> + 10 +
+
+
+ + {/* Duration Range */} +
+ +
+
+
+ + handleDurationChange('min', e.target.value)} + className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-red-500 focus:outline-none transition-colors" + /> +
+
+ + handleDurationChange('max', e.target.value)} + className="w-full px-3 py-2 bg-gray-700 text-white rounded-lg border border-gray-600 focus:border-red-500 focus:outline-none transition-colors" + /> +
+
+
+ 0m + handleDurationChange('min', e.target.value)} + className="flex-1 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + /> + handleDurationChange('max', e.target.value)} + className="flex-1 h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + /> + 5h +
+
+
+
+
+ ) +} + +export default FilterPanel diff --git a/frontend/src/components/MovieModal.jsx b/frontend/src/components/MovieModal.jsx new file mode 100644 index 0000000..27af6bc --- /dev/null +++ b/frontend/src/components/MovieModal.jsx @@ -0,0 +1,209 @@ +import React, { useState } from 'react' +import VideoPlayer from './VideoPlayer' + +function MovieModal({ movie, isOpen, onClose }) { + const [showVideoPlayer, setShowVideoPlayer] = useState(false) + + if (!isOpen || !movie) return null + + const handlePlayTrailer = () => { + setShowVideoPlayer(true) + } + + const handleCloseVideo = () => { + setShowVideoPlayer(false) + } + + const handleAddToList = () => { + // TODO: Implement add to watchlist functionality + console.log('Added to list:', movie.title) + } + + const handleLike = () => { + // TODO: Implement like functionality + console.log('Liked:', movie.title) + } + + const handleBackdropClick = (e) => { + // Close modal if clicking on backdrop (not on modal content) + if (e.target === e.currentTarget) { + onClose() + } + } + + const handleModalContentClick = (e) => { + // Prevent clicks inside modal content from bubbling up to backdrop + e.stopPropagation() + } + + return ( + <> + {/* Movie Info Modal */} + {!showVideoPlayer && ( +
+
+ {/* Header with backdrop image */} +
+ {movie.title} +
+ + {/* Close button */} + + + {/* Movie info overlay */} +
+

{movie.title}

+
+ + {Math.round(movie.rating * 10)}% Match + + {movie.release_year} + {movie.rating && ( +
+ + + + {movie.rating} +
+ )} +
+ + {/* Action buttons */} +
+ + + + + +
+
+
+ + {/* Movie details */} +
+
+ {/* Main content */} +
+

About {movie.title}

+

+ {movie.description || 'No description available for this movie.'} +

+ + {/* Cast and crew (mock data) */} +
+

Cast

+

+ {movie.cast?.join(', ') || 'Cast information not available'} +

+
+ + {movie.director && ( +
+

Director

+

{movie.director}

+
+ )} +
+ + {/* Sidebar */} +
+
+ {movie.genres && ( +
+

Genres

+
+ {movie.genres.map((genre, index) => ( + + {genre} + + ))} +
+
+ )} + +
+

Release Year

+

{movie.release_year}

+
+ + {movie.duration && ( +
+

Duration

+

{movie.duration} minutes

+
+ )} + + {movie.rating && ( +
+

Rating

+
+ + + + {movie.rating}/10 +
+
+ )} +
+
+
+
+
+
+ )} + + {/* Video Player */} + + + ) +} + +export default MovieModal diff --git a/frontend/src/components/MovieRow.jsx b/frontend/src/components/MovieRow.jsx index 88b9746..aec1b15 100644 --- a/frontend/src/components/MovieRow.jsx +++ b/frontend/src/components/MovieRow.jsx @@ -1,28 +1,77 @@ import React from 'react' -function MovieRow({ title, movies }) { +function MovieRow({ title, movies, showTitle = true, onMovieClick }) { + const handleMovieClick = (movie) => { + if (onMovieClick) { + onMovieClick(movie) + } + } + + const handlePlayClick = (e, movie) => { + e.stopPropagation() // Prevent triggering the movie click + handleMovieClick(movie) + } + return (
-

{title}

+ {showTitle &&

{title}

}
-
+
{movies.map((movie) => ( -
-
+
handleMovieClick(movie)} + > +
{movie.title} -
- + + {/* Hover Overlay */} +
+
+ +
+
+ + {/* Movie Info Overlay */} +
+

{movie.title}

+
+ {movie.release_year} + {movie.rating && ( + <> + β€’ +
+ + + + {movie.rating} +
+ + )} +
+ {movie.description && ( +

+ {movie.description.length > 80 + ? `${movie.description.substring(0, 80)}...` + : movie.description + } +

+ )}
-

{movie.title}

))}
@@ -31,4 +80,4 @@ function MovieRow({ title, movies }) { ) } -export default MovieRow \ No newline at end of file +export default MovieRow diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 84433ac..1f60530 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,35 +1,102 @@ -import React from 'react' +import React, { useState } from 'react' +import SearchBar from './SearchBar' +import UserProfileDropdown from './UserProfileDropdown' +import { mockUserProfile } from '../data/mockUser' + +function Navbar({ onSearchResults, onSearchToggle, isSearchActive }) { + const [showSearch, setShowSearch] = useState(false) + const [showProfileDropdown, setShowProfileDropdown] = useState(false) + + const handleSearchToggle = () => { + const newSearchState = !showSearch + setShowSearch(newSearchState) + onSearchToggle?.(newSearchState) + } + + const handleSearchResults = (results) => { + onSearchResults?.(results) + } + + const handleSearchClose = () => { + setShowSearch(false) + onSearchToggle?.(false) + onSearchResults?.([]) + } + + const handleProfileClick = () => { + setShowProfileDropdown(!showProfileDropdown) + } + + const handleProfileClose = () => { + setShowProfileDropdown(false) + } -function Navbar() { return ( -