diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f0749cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Git Attributes for docs-sample-apps +# Marks auto-generated files that are committed to version control +# so they're collapsed in GitHub PR diffs and excluded from code review + +# Maven Wrapper - Auto-generated by Apache Maven (committed to git) +# See: https://maven.apache.org/wrapper/ +server/java-spring/mvnw linguist-generated=true +server/java-spring/mvnw.cmd linguist-generated=true diff --git a/.github/scripts/generate-test-summary-jest.sh b/.github/scripts/generate-test-summary-jest.sh new file mode 100644 index 0000000..48e86bf --- /dev/null +++ b/.github/scripts/generate-test-summary-jest.sh @@ -0,0 +1,122 @@ +#!/bin/bash +set -e + +# Generate Detailed Test Summary from Multiple Jest JSON Output Files +# Shows breakdown by test type (unit vs integration) +# Usage: ./generate-test-summary-jest.sh + +UNIT_JSON="${1:-}" +INTEGRATION_JSON="${2:-}" + +echo "## Test Results" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# Function to parse Jest JSON file +parse_json() { + local json_file="$1" + local test_type="$2" + + if [ ! -f "$json_file" ]; then + echo "0 0 0 0" + return + fi + + if command -v jq &> /dev/null; then + # Use jq if available (preferred) + total_tests=$(jq -r '.numTotalTests // 0' "$json_file") + passed=$(jq -r '.numPassedTests // 0' "$json_file") + failed=$(jq -r '.numFailedTests // 0' "$json_file") + skipped=$(jq -r '.numPendingTests // 0' "$json_file") + else + # Fallback to grep/sed if jq is not available + total_tests=$(grep -oP '"numTotalTests":\s*\K[0-9]+' "$json_file" | head -1) + passed=$(grep -oP '"numPassedTests":\s*\K[0-9]+' "$json_file" | head -1) + failed=$(grep -oP '"numFailedTests":\s*\K[0-9]+' "$json_file" | head -1) + skipped=$(grep -oP '"numPendingTests":\s*\K[0-9]+' "$json_file" | head -1) + fi + + # Default to 0 if values are empty + total_tests=${total_tests:-0} + passed=${passed:-0} + failed=${failed:-0} + skipped=${skipped:-0} + + echo "$total_tests $passed $failed $skipped" +} + +# Parse both files +read -r unit_tests unit_passed unit_failed unit_skipped <<< "$(parse_json "$UNIT_JSON" "Unit")" +read -r int_tests int_passed int_failed int_skipped <<< "$(parse_json "$INTEGRATION_JSON" "Integration")" + +# Calculate totals +total_tests=$((unit_tests + int_tests)) +total_passed=$((unit_passed + int_passed)) +total_failed=$((unit_failed + int_failed)) +total_skipped=$((unit_skipped + int_skipped)) + +# Display detailed breakdown +echo "### Summary by Test Type" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY +echo "| Test Type | Passed | Failed | Skipped | Total |" >> $GITHUB_STEP_SUMMARY +echo "|-----------|--------|--------|---------|-------|" >> $GITHUB_STEP_SUMMARY + +if [ -f "$UNIT_JSON" ]; then + echo "| 🔧 Unit Tests | $unit_passed | $unit_failed | $unit_skipped | $unit_tests |" >> $GITHUB_STEP_SUMMARY +fi + +if [ -f "$INTEGRATION_JSON" ]; then + echo "| 🔗 Integration Tests | $int_passed | $int_failed | $int_skipped | $int_tests |" >> $GITHUB_STEP_SUMMARY +fi + +echo "| **Total** | **$total_passed** | **$total_failed** | **$total_skipped** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# Overall status +echo "### Overall Status" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY +echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY +echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY +echo "| ✅ Passed | $total_passed |" >> $GITHUB_STEP_SUMMARY +echo "| ❌ Failed | $total_failed |" >> $GITHUB_STEP_SUMMARY +echo "| ⏭️ Skipped | $total_skipped |" >> $GITHUB_STEP_SUMMARY +echo "| **Total** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# List failed tests if any +if [ $total_failed -gt 0 ]; then + echo "### ❌ Failed Tests" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + failed_tests_file=$(mktemp) + + # Extract failed tests from both files + for json_file in "$UNIT_JSON" "$INTEGRATION_JSON"; do + if [ -f "$json_file" ]; then + if command -v jq &> /dev/null; then + jq -r '.testResults[]? | select(.status == "failed") | .assertionResults[]? | select(.status == "failed") | "\(.ancestorTitles | join(" > ")) > \(.title)"' "$json_file" >> "$failed_tests_file" 2>/dev/null || true + else + # Basic fallback without jq + grep -oP '"fullName":\s*"\K[^"]*' "$json_file" | while read -r line; do + if echo "$line" | grep -q "failed"; then + echo "$line" >> "$failed_tests_file" + fi + done 2>/dev/null || true + fi + fi + done + + if [ -s "$failed_tests_file" ]; then + while IFS= read -r test; do + echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY + done < "$failed_tests_file" + else + echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY + rm -f "$failed_tests_file" + exit 1 +else + echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY +fi diff --git a/.github/scripts/generate-test-summary-pytest.sh b/.github/scripts/generate-test-summary-pytest.sh new file mode 100755 index 0000000..9b1bef1 --- /dev/null +++ b/.github/scripts/generate-test-summary-pytest.sh @@ -0,0 +1,109 @@ +#!/bin/bash +set -e + +# Generate Detailed Test Summary from Multiple Pytest JUnit XML Output Files +# Shows breakdown by test type (unit vs integration) +# Usage: ./generate-test-summary-pytest-detailed.sh + +UNIT_XML="${1:-}" +INTEGRATION_XML="${2:-}" + +echo "## Test Results" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# Function to parse XML file +parse_xml() { + local xml_file="$1" + local test_type="$2" + + if [ ! -f "$xml_file" ]; then + echo "0 0 0 0 0" + return + fi + + tests=$(grep -oP 'tests="\K[0-9]+' "$xml_file" | head -1) + failures=$(grep -oP 'failures="\K[0-9]+' "$xml_file" | head -1) + errors=$(grep -oP 'errors="\K[0-9]+' "$xml_file" | head -1) + skipped=$(grep -oP 'skipped="\K[0-9]+' "$xml_file" | head -1) + + tests=${tests:-0} + failures=${failures:-0} + errors=${errors:-0} + skipped=${skipped:-0} + passed=$((tests - failures - errors - skipped)) + + echo "$tests $failures $errors $skipped $passed" +} + +# Parse both files +read -r unit_tests unit_failures unit_errors unit_skipped unit_passed <<< "$(parse_xml "$UNIT_XML" "Unit")" +read -r int_tests int_failures int_errors int_skipped int_passed <<< "$(parse_xml "$INTEGRATION_XML" "Integration")" + +# Calculate totals +total_tests=$((unit_tests + int_tests)) +total_failures=$((unit_failures + int_failures)) +total_errors=$((unit_errors + int_errors)) +total_skipped=$((unit_skipped + int_skipped)) +total_passed=$((unit_passed + int_passed)) +total_failed=$((total_failures + total_errors)) + +# Display detailed breakdown +echo "### Summary by Test Type" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY +echo "| Test Type | Passed | Failed | Skipped | Total |" >> $GITHUB_STEP_SUMMARY +echo "|-----------|--------|--------|---------|-------|" >> $GITHUB_STEP_SUMMARY + +if [ -f "$UNIT_XML" ]; then + echo "| 🔧 Unit Tests | $unit_passed | $((unit_failures + unit_errors)) | $unit_skipped | $unit_tests |" >> $GITHUB_STEP_SUMMARY +fi + +if [ -f "$INTEGRATION_XML" ]; then + echo "| 🔗 Integration Tests | $int_passed | $((int_failures + int_errors)) | $int_skipped | $int_tests |" >> $GITHUB_STEP_SUMMARY +fi + +echo "| **Total** | **$total_passed** | **$total_failed** | **$total_skipped** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# Overall status +echo "### Overall Status" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY +echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY +echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY +echo "| ✅ Passed | $total_passed |" >> $GITHUB_STEP_SUMMARY +echo "| ❌ Failed | $total_failed |" >> $GITHUB_STEP_SUMMARY +echo "| ⏭️ Skipped | $total_skipped |" >> $GITHUB_STEP_SUMMARY +echo "| **Total** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# List failed tests if any +if [ $total_failed -gt 0 ]; then + echo "### ❌ Failed Tests" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + failed_tests_file=$(mktemp) + + # Extract failed tests from both files + for xml_file in "$UNIT_XML" "$INTEGRATION_XML"; do + if [ -f "$xml_file" ]; then + grep -oP ']*classname="[^"]*"[^>]*name="[^"]*"[^>]*>.*?<(failure|error)' "$xml_file" | \ + grep -oP 'classname="\K[^"]*|name="\K[^"]*' | \ + paste -d '.' - - >> "$failed_tests_file" 2>/dev/null || true + fi + done + + if [ -s "$failed_tests_file" ]; then + while IFS= read -r test; do + echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY + done < "$failed_tests_file" + else + echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY + rm -f "$failed_tests_file" + exit 1 +else + echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY +fi + diff --git a/.github/scripts/generate-test-summary-surefire.sh b/.github/scripts/generate-test-summary-surefire.sh new file mode 100644 index 0000000..fee0e56 --- /dev/null +++ b/.github/scripts/generate-test-summary-surefire.sh @@ -0,0 +1,82 @@ +#!/bin/bash +set -e + +# Generate Test Summary from Maven Surefire Reports +# Usage: ./generate-test-summary-surefire.sh + +REPORTS_DIR="${1:-target/surefire-reports}" + +echo "## Test Results" >> $GITHUB_STEP_SUMMARY +echo "" >> $GITHUB_STEP_SUMMARY + +# Parse test results from Surefire reports +if [ -d "$REPORTS_DIR" ]; then + total_tests=0 + failures=0 + errors=0 + skipped=0 + failed_tests_file=$(mktemp) + + for file in "$REPORTS_DIR"/TEST-*.xml; do + if [ -f "$file" ]; then + # Extract test counts from XML + tests=$(grep -oP 'tests="\K[0-9]+' "$file" | head -1) + fails=$(grep -oP 'failures="\K[0-9]+' "$file" | head -1) + errs=$(grep -oP 'errors="\K[0-9]+' "$file" | head -1) + skip=$(grep -oP 'skipped="\K[0-9]+' "$file" | head -1) + + total_tests=$((total_tests + ${tests:-0})) + failures=$((failures + ${fails:-0})) + errors=$((errors + ${errs:-0})) + skipped=$((skipped + ${skip:-0})) + + # Extract failed test cases + if [ "${fails:-0}" -gt 0 ] || [ "${errs:-0}" -gt 0 ]; then + classname=$(basename "$file" .xml | sed 's/^TEST-//') + + # Find failed testcases (with failure or error elements) + grep -oP ']*name="[^"]*"[^>]*>.*?<(failure|error)' "$file" | \ + grep -oP 'name="\K[^"]*' | while read -r testname; do + echo "$classname.$testname" >> "$failed_tests_file" + done + fi + fi + done + + passed=$((total_tests - failures - errors - skipped)) + + echo "| Status | Count |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| ✅ Passed | $passed |" >> $GITHUB_STEP_SUMMARY + echo "| ❌ Failed | $((failures + errors)) |" >> $GITHUB_STEP_SUMMARY + echo "| ⏭️ Skipped | $skipped |" >> $GITHUB_STEP_SUMMARY + echo "| **Total** | **$total_tests** |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # List failed tests if any + if [ $((failures + errors)) -gt 0 ]; then + echo "### ❌ Failed Tests" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -s "$failed_tests_file" ]; then + while IFS= read -r test; do + echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY + done < "$failed_tests_file" + else + echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY + rm -f "$failed_tests_file" + exit 1 + else + echo "✅ **All tests passed!**" >> $GITHUB_STEP_SUMMARY + fi + + rm -f "$failed_tests_file" +else + echo "⚠️ No test results found at: $REPORTS_DIR" >> $GITHUB_STEP_SUMMARY + exit 1 +fi + diff --git a/.github/workflows/run-express-tests.yml b/.github/workflows/run-express-tests.yml new file mode 100644 index 0000000..36457cc --- /dev/null +++ b/.github/workflows/run-express-tests.yml @@ -0,0 +1,70 @@ +name: Run Express Tests + +on: + pull_request_target: + branches: + - development + paths: + - 'server/js-express/**' + push: + branches: + - development + paths: + - 'server/js-express/**' + +jobs: + test: + name: Run Express Tests + runs-on: ubuntu-latest + # Require manual approval for fork PRs + environment: testing + + defaults: + run: + working-directory: server/js-express + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: npm install + + - name: Run unit tests + run: npm run test:unit -- --json --outputFile=test-results-unit.json || true + env: + MONGODB_URI: ${{ secrets.MFLIX_URI }} + + - name: Run integration tests + run: npm run test:integration -- --json --outputFile=test-results-integration.json || true + env: + MONGODB_URI: ${{ secrets.MFLIX_URI }} + ENABLE_SEARCH_TESTS: true + VOYAGE_API_KEY: ${{ secrets.VOYAGE_AI }} + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: | + server/js-express/coverage/ + server/js-express/test-results-unit.json + server/js-express/test-results-integration.json + retention-days: 30 + + - name: Generate Test Summary + if: always() + working-directory: . + run: | + chmod +x .github/scripts/generate-test-summary-jest.sh + .github/scripts/generate-test-summary-jest.sh \ + server/js-express/test-results-unit.json \ + server/js-express/test-results-integration.json diff --git a/.github/workflows/run-java-spring-boot-tests.yml b/.github/workflows/run-java-spring-boot-tests.yml new file mode 100644 index 0000000..6c4ffa9 --- /dev/null +++ b/.github/workflows/run-java-spring-boot-tests.yml @@ -0,0 +1,66 @@ +name: Run Java Spring Boot Tests + +on: + pull_request_target: + branches: + - development + paths: + - 'mflix/server/java-spring/**' + push: + branches: + - development + paths: + - 'mflix/server/java-spring/**' + +jobs: + test: + name: Run Java Spring Boot Tests + runs-on: ubuntu-latest + # Require manual approval for fork PRs + environment: testing + + defaults: + run: + working-directory: mflix/server/java-spring + + env: + MONGODB_URI: ${{ secrets.MFLIX_URI }} + ENABLE_SEARCH_TESTS: true + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Make mvnw executable + run: chmod +x mvnw + + - name: Run unit tests + run: ./mvnw test + + - name: Run integration tests + run: ./mvnw test -Dtest=MongoDBSearchIntegrationTest + continue-on-error: true + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: mflix/server/java-spring/target/surefire-reports/ + retention-days: 30 + + - name: Generate Test Summary + if: always() + working-directory: . + run: | + chmod +x .github/scripts/generate-test-summary-surefire.sh + .github/scripts/generate-test-summary-surefire.sh mflix/server/java-spring/target/surefire-reports diff --git a/.github/workflows/run-python-tests.yml b/.github/workflows/run-python-tests.yml new file mode 100644 index 0000000..11b43cc --- /dev/null +++ b/.github/workflows/run-python-tests.yml @@ -0,0 +1,73 @@ +name: Run Python Tests + +on: + pull_request_target: + branches: + - development + paths: + - 'mflix/server/python-fastapi/**' + push: + branches: + - development + paths: + - 'mflix/server/python-fastapi/**' + +jobs: + test: + name: Run Python Tests + runs-on: ubuntu-latest + # Require manual approval for fork PRs + environment: testing + + defaults: + run: + working-directory: mflix/server/python-fastapi + + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run unit tests + run: pytest -m unit --verbose --tb=short --junit-xml=test-results-unit.xml + env: + MONGO_URI: ${{ secrets.MFLIX_URI }} + MONGO_DB: sample_mflix + + - name: Run integration tests + run: pytest -m integration --verbose --tb=short --junit-xml=test-results-integration.xml || true + env: + MONGO_URI: ${{ secrets.MFLIX_URI }} + MONGO_DB: sample_mflix + + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: | + mflix/server/python-fastapi/test-results-unit.xml + mflix/server/python-fastapi/test-results-integration.xml + mflix/server/python-fastapi/htmlcov/ + retention-days: 30 + + - name: Generate Test Summary + if: always() + working-directory: . + run: | + chmod +x .github/scripts/generate-test-summary-pytest.sh + .github/scripts/generate-test-summary-pytest.sh \ + mflix/server/python-fastapi/test-results-unit.xml \ + mflix/server/python-fastapi/test-results-integration.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e90af98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# ============================================================================= +# Root .gitignore for MongoDB Sample Apps Monorepo +# ============================================================================= +# This file contains patterns common to all projects in the monorepo. +# Backend-specific patterns are defined in each backend's .gitignore file. +# ============================================================================= + +# Environment Variables (Global) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local +!.env.example + +# Operating System Files +.DS_Store +Thumbs.db + +# IDE and Editor Files (Global) +.idea/ +.vscode/ +*.swp +*.swo +*.swn +*.bak +*.tmp +*.iml +.project +.classpath +.settings/ +*.sublime-project +*.sublime-workspace + +# Logs (Global) +logs/ +*.log + +# Temporary Files (Global) +*.tmp +*.temp +.cache/ + +# Node.js (Global - applies to all Node.js projects) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Test Coverage (Global) +coverage/ +*.lcov +.nyc_output diff --git a/mflix/.gitignore-java b/mflix/.gitignore-java new file mode 100644 index 0000000..9e08a98 --- /dev/null +++ b/mflix/.gitignore-java @@ -0,0 +1,113 @@ +#------- +# Common +#------- + +# Environment variables +.env +!.env.example +*.pem + +#MacOS files +.DS_Store + +#--------------------------- +# Java / Spring Boot backend +#--------------------------- + +#Cache +.cache/ + +# Compiled Files +*.class +*.ctxt +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear + +# Crash Logs +hs_err_pid* +replay_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IntelliJ IDEA +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +# Eclipse +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +# NetBeans +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +# Spring Boot +spring-boot-devtools.properties + +#------------------------ +# Next.js Client frontend +#------------------------ + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions +package-lock.json + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/mflix/.gitignore-js b/mflix/.gitignore-js new file mode 100644 index 0000000..aa54d26 --- /dev/null +++ b/mflix/.gitignore-js @@ -0,0 +1,62 @@ +#------- +# Common +#------- + +# Environment variables +.env +!.env.example +*.pem + +#MacOS files +.DS_Store + +# Dependencies +node_modules/ +package-lock.json +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# TypeScript +*.tsbuildinfo + +# Test coverage +coverage/ + +# Logs +logs +*.log + +#------------------- +# Express.js Backend +#------------------- + +# Build output +dist/ + +# Test results +test-results.json + +# Optional npm cache directory +.npm + +#------------------------ +# Next.js Client frontend +#------------------------ + +# next.js +/.next/ +/out/ + +# production +/build + +# vercel +.vercel + +# typescript +next-env.d.ts diff --git a/mflix/.gitignore-python b/mflix/.gitignore-python new file mode 100644 index 0000000..41f9a7e --- /dev/null +++ b/mflix/.gitignore-python @@ -0,0 +1,52 @@ +#------- +# Common +#------- + +# Environment variables +.env +!.env.example +*.pem + +#MacOS files +.DS_Store + +#------------------------- +# Python / FastAPI backend +#------------------------- + +# Byte-compiled / optimized files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +.venv/ + +#------------------------ +# Next.js Client frontend +#------------------------ + +# dependencies +/node_modules +package-lock.json + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# Logs +logs +*.log + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/mflix/README-JAVA-SPRING.md b/mflix/README-JAVA-SPRING.md new file mode 100644 index 0000000..d73a717 --- /dev/null +++ b/mflix/README-JAVA-SPRING.md @@ -0,0 +1,200 @@ +# Java Spring Boot MongoDB Sample MFlix Application + +This is a full-stack movie browsing application built with Java Spring Boot and Next.js, demonstrating MongoDB operations using the `sample_mflix` dataset. The application showcases CRUD operations, aggregations, and MongoDB Search using Spring Data MongoDB. + +## Project Structure + +``` +├── README.md +├── client/ # Next.js frontend (TypeScript) +└── server/ # Java Spring Boot backend + ├── src/ + ├── pom.xml + ├── .env.example + └── mvnw +``` + +## Prerequisites + +- **Java 21** or higher +- **Node.js 20** or higher +- **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded + - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) +- **Maven** (included via Maven Wrapper) +- **Voyage AI API key** (For MongoDB Vector Search) + - [Get a Voyage AI API key](https://www.voyageai.com/) + +## Getting Started + +### 1. Configure the Backend + +Navigate to the Java Spring server directory: + +```bash +cd server +``` + +Create a `.env` file from the example: + +```bash +cp .env.example .env +``` + +Edit the `.env` file and set your MongoDB connection string: + +```env +# MongoDB Connection +# Replace with your MongoDB Atlas connection string or local MongoDB URI +MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority + +# Voyage AI Configuration +# API key for Voyage AI embedding model (required for Vector Search) +VOYAGE_API_KEY=your_voyage_api_key + +# Server Configuration +# Port on which the Spring Boot application will run +PORT=3001 + +# CORS Configuration +# Allowed origin for cross-origin requests (frontend URL) +# For multiple origins, separate with commas +CORS_ORIGIN=http://localhost:3000 + +# Optional: Enable MongoDB Search tests +# Uncomment the following line to enable Search tests +# ENABLE_SEARCH_TESTS=true +``` + +**Note:** Replace `username`, `password`, and `cluster` with your +actual MongoDB Atlas credentials. Replace `your_voyage_api_key` with +your key. + +### 2. Start the Backend Server + +From the `server` directory, run: + +```bash +# Using Maven Wrapper (recommended) +./mvnw spring-boot:run + +# Or on Windows +mvnw.cmd spring-boot:run +``` + +The server will start on `http://localhost:3001`. You can verify it's running by visiting: +- API root: http://localhost:3001/ +- API documentation (Swagger UI): http://localhost:3001/swagger-ui.html + +### 3. Configure and Start the Frontend + +Open a new terminal and navigate to the client directory: + +```bash +cd client +``` + +Install dependencies: + +```bash +npm install +``` + +Start the development server: + +```bash +npm run dev +``` + +The Next.js application will start on `http://localhost:3000`. + +### 4. Access the Application + +Open your browser and navigate to: +- **Frontend:** http://localhost:3000 +- **Backend API:** http://localhost:3001 +- **API Documentation:** http://localhost:3001/swagger-ui.html + +## Features + +- **Browse Movies:** View a paginated list of movies from the + sample_mflix dataset +- **CRUD Operations:** Create, read, update and delete movies by using + the MongoDB Java driver +- **Search:** Search movies with filters by using MongoDB Search +- **Vector Search:** Search movie plots with similar search terms by + using MongoDB Vector Search +- **Aggregations:** View data aggregations and analytics built with + aggregation pipelines + +## Development + +### Backend Development + +The Java Spring Boot backend uses: +- **Spring Data MongoDB** for database operations +- **Spring Boot Web** for REST API +- **SpringDoc OpenAPI** for API documentation +- **Maven** for dependency management + +To run tests: + +```bash +cd server +./mvnw test +``` + +### Frontend Development + +The Next.js frontend uses: +- **React 19** with TypeScript +- **Next.js 16** with App Router +- **Turbopack** for fast development builds + +#### Development Mode + +For active development with hot reloading and fast refresh: + +```bash +cd client +npm run dev +``` + +This starts the development server on `http://localhost:3000` with Turbopack for fast rebuilds. + +#### Production Build + +To create an optimized production build and run it: + +```bash +cd client +npm run build # Creates optimized production build +npm start # Starts production server +``` + +The production build: +- Minifies and optimizes JavaScript and CSS +- Optimizes images and assets +- Generates static pages where possible +- Provides better performance for end users + +#### Linting + +To check code quality: + +```bash +cd client +npm run lint +``` + +## Issues + +If you have problems running the sample app, please check the following: + +- [ ] Verify that you have set your MongoDB connection string in the `.env` file. +- [ ] Verify that you have started the Java Spring server. +- [ ] Verify that you have started the Next.js client. +- [ ] Verify that you have no firewalls blocking access to the server or client ports. + +If you have verified the above and still have issues, please +[open an issue](https://github.com/mongodb/docs-sample-apps/issues/new/choose) +on the source repository `mongodb/docs-sample-apps`. diff --git a/mflix/README-JAVASCRIPT-EXPRESS.md b/mflix/README-JAVASCRIPT-EXPRESS.md new file mode 100644 index 0000000..130bf5f --- /dev/null +++ b/mflix/README-JAVASCRIPT-EXPRESS.md @@ -0,0 +1,220 @@ +# JavaScript Express.js MongoDB Sample MFlix Application + +This is a full-stack movie browsing application built with Express.js and Next.js, demonstrating MongoDB operations using the `sample_mflix` dataset. The application showcases CRUD operations, aggregations, and MongoDB Search using the native MongoDB Node.js driver. + +## Project Structure + +``` +├── README.md +├── client/ # Next.js frontend (TypeScript) +└── server # Express.js backend + ├── src/ + ├── package.json + ├── .env.example + └── tsconfig.json +``` + +## Prerequisites + +- **Node.js 22** or higher +- **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded + - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) +- **npm** (included with Node.js) +- **Voyage AI API key** (For MongoDB Vector Search) + - [Get a Voyage AI API key](https://www.voyageai.com/) + +## Getting Started + +### 1. Configure the Backend + +Navigate to the Express server directory: + +```bash +cd server +``` + +Create a `.env` file from the example: + +```bash +cp .env.example .env +``` + +Edit the `.env` file and set your MongoDB connection string: + +```env +# MongoDB Connection +# Replace with your MongoDB Atlas connection string or local MongoDB URI +MONGODB_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority + +# Voyage AI Configuration +# API key for Voyage AI embedding model (required for Vector Search) +VOYAGE_API_KEY=your_voyage_api_key + +# Server Configuration +PORT=3001 +NODE_ENV=development + +# CORS Configuration +# Allowed origin for cross-origin requests (frontend URL) +# For multiple origins, separate with commas +CORS_ORIGIN=http://localhost:3000 + +# Optional: Enable MongoDB Search tests +# Uncomment the following line to enable Search tests +# ENABLE_SEARCH_TESTS=true +``` + +**Note:** Replace ``, ``, and `` with +your actual MongoDB Atlas credentials. Replace `your_voyage_api_key` with +your key. + +### 2. Install Backend Dependencies + +From the `server` directory, run: + +```bash +npm install +``` + +### 3. Start the Backend Server + +From the `server` directory, run: + +```bash +# Development mode with hot reloading +npm run dev +``` + + +Or for production mode, run: + +```bash +npm run build +npm start +``` + +The server will start on `http://localhost:3001`. You can verify it's running by visiting: +- API root: http://localhost:3001/ +- API documentation (Swagger UI): http://localhost:3001/api-docs + +### 4. Configure and Start the Frontend + +Open a new terminal and navigate to the client directory: + +```bash +cd client +``` + +Install dependencies: + +```bash +npm install +``` + +Start the development server: + +```bash +npm run dev +``` + +The Next.js application will start on `http://localhost:3000`. + +### 5. Access the Application + +Open your browser and navigate to: +- **Frontend:** http://localhost:3000 +- **Backend API:** http://localhost:3001 +- **API Documentation:** http://localhost:3001/api-docs + +## Features + +- **Browse Movies:** View a paginated list of movies from the + sample_mflix dataset +- **CRUD Operations:** Create, read, update and delete movies by using + the MongoDB Node.js driver +- **Search:** Search movies with filters by using MongoDB Search +- **Vector Search:** Search movie plots with similar search terms by + using MongoDB Vector Search +- **Aggregations:** View data aggregations and analytics built with + aggregation pipelines + +## Development + +### Backend Development + +The Express.js backend uses: +- **Express.js 5** for REST API +- **MongoDB Node.js Driver** for database operations +- **TypeScript** for type safety +- **Swagger** for API documentation +- **Jest** for testing + +To run tests: + +```bash +cd server +npm test +``` + +To run tests with coverage: + +```bash +cd server +npm run test:coverage +``` + +### Frontend Development + +The Next.js frontend uses: +- **React 19** with TypeScript +- **Next.js 16** with App Router +- **Turbopack** for fast development builds + +#### Development Mode + +For active development with hot reloading and fast refresh: + +```bash +cd client +npm run dev +``` + +This starts the development server on `http://localhost:3000` with Turbopack for fast rebuilds. + +#### Production Build + +To create an optimized production build and run it: + +```bash +cd client +npm run build # Creates optimized production build +npm start # Starts production server +``` + +The production build: +- Minifies and optimizes JavaScript and CSS +- Optimizes images and assets +- Generates static pages where possible +- Provides better performance for end users + +#### Linting + +To check code quality: + +```bash +cd client +npm run lint +``` + +## Issues + +If you have problems running the sample app, please check the following: + +- [ ] Verify that you have set your MongoDB connection string in the `.env` file. +- [ ] Verify that you have started the Express server. +- [ ] Verify that you have started the Next.js client. +- [ ] Verify that you have no firewalls blocking access to the server or client ports. + +If you have verified the above and still have issues, please +[open an issue](https://github.com/mongodb/docs-sample-apps/issues/new/choose) +on the source repository `mongodb/docs-sample-apps`. diff --git a/mflix/README-PYTHON-FASTAPI.md b/mflix/README-PYTHON-FASTAPI.md new file mode 100644 index 0000000..f3be9de --- /dev/null +++ b/mflix/README-PYTHON-FASTAPI.md @@ -0,0 +1,207 @@ +# Python FastAPI MongoDB Sample MFlix Application + +This is a full-stack movie browsing application built with Python FastAPI and Next.js, demonstrating MongoDB operations using the `sample_mflix` dataset. The application showcases CRUD operations, aggregations, and MongoDB Search using the PyMongo driver. + +## Project Structure + +``` +├── README.md +├── client/ # Next.js frontend (TypeScript) +└── server/ # Python FastAPI backend + ├── src/ + ├── tests/ + ├── .env.example + ├── main.py + ├── pytest.ini + ├── requirements.in + └── requirements.txt +``` + +## Prerequisites + +- **Python 3.10** to **Python 3.13** +- **Node.js 20** or higher +- **MongoDB Atlas cluster or local deployment** with the `sample_mflix` dataset loaded + - [Load sample data](https://www.mongodb.com/docs/atlas/sample-data/) +- **pip** for Python package management +- **Voyage AI API key** (For MongoDB Vector Search) + - [Get a Voyage AI API key](https://www.voyageai.com/) + +## Getting Started + +### 1. Configure the Backend + +Navigate to the Python FastAPI server directory: + +```bash +cd server/ +``` + +Create a `.env` file from the example: + +```bash +cp .env.example .env +``` + +Edit the `.env` file and set your MongoDB connection string: + +```env +# MongoDB Configuration +MONGO_URI=mongodb+srv://:@.mongodb.net/sample_mflix?retryWrites=true&w=majority +MONGO_DB=sample_mflix + +# Voyage AI Configuration +# API key for Voyage AI embedding model (required for Vector Search) +VOYAGE_API_KEY=your_voyage_api_key + +# CORS Configuration +# Comma-separated list of allowed origins for CORS +CORS_ORIGINS=http://localhost:3000,http://localhost:3001 +``` + +**Note:** Replace `username`, `password`, and `cluster` with your actual MongoDB Atlas +credentials. + +Make a virtual environment: + +```bash +python -m venv .venv +``` + +Activate the virtual environment: + +```bash +source .venv/bin/activate +``` + +Install Python dependencies: + +```bash +pip install -r requirements.txt +``` + +### 2. Start the Backend Server + +From the `server/` directory, run: + +```bash +uvicorn main:app --reload --port 3001 +``` + +The server will start on `http://localhost:3001`. You can verify it's running by visiting: +- API root: http://localhost:3001/api/movies +- API documentation (Swagger UI): http://localhost:3001/docs +- Interactive API documentation (ReDoc): http://localhost:3001/redoc + +### 3. Configure and Start the Frontend + +Open a new terminal and navigate to the client directory: + +```bash +cd client +``` + +Install dependencies: + +```bash +npm install +``` + +Start the development server: + +```bash +npm run dev +``` + +The Next.js application will start on `http://localhost:3000`. + +### 4. Access the Application + +Open your browser and navigate to: +- **Frontend:** http://localhost:3000 +- **Backend API:** http://localhost:3001 +- **API Documentation:** http://localhost:3001/docs + +## Features + +- **Browse Movies:** View a paginated list of movies from the sample_mflix dataset +- **Search:** Full-text search using MongoDB Search +- **Vector Search:** Semantic search using MongoDB Vector Search with Voyage AI embeddings +- **Filter:** Filter movies by genre, year, rating, and more +- **Movie Details:** View detailed information about each movie +- **Aggregations:** Complex data aggregations and analytics + +## Development + +### Backend Development + +The Python FastAPI backend uses: +- **FastAPI** for REST API framework +- **PyMongo** for database operations +- **Voyage AI** for vector embeddings +- **fastapi** for ASGI server + +To run all tests: + +```bash +cd server/ +source .venv/bin/activate # or `.venv\Scripts\activate` on Windows +pytest tests/ -v +``` + +### Frontend Development + +The Next.js frontend uses: +- **React 19** with TypeScript +- **Next.js 16** with App Router +- **Turbopack** for fast development builds + +#### Development Mode + +For active development with hot reloading and fast refresh: + +```bash +cd client +npm run dev +``` + +This starts the development server on `http://localhost:3000` with Turbopack for fast rebuilds. + +#### Production Build + +To create an optimized production build and run it: + +```bash +cd client +npm run build # Creates optimized production build +npm start # Starts production server +``` + +The production build: +- Minifies and optimizes JavaScript and CSS +- Optimizes images and assets +- Generates static pages where possible +- Provides better performance for end users + +#### Linting + +To check code quality: + +```bash +cd client +npm run lint +``` + +## Issues + +If you have problems running the sample app, please check the following: + +- [ ] Verify that you have set your MongoDB connection string in the `.env` file. +- [ ] Verify that you have created and activated a Python `.venv` on Python v3.10 through v3.13. +- [ ] Verify that you have started the Python FastAPI server. +- [ ] Verify that you have started the Next.js client. +- [ ] Verify that you have no firewalls blocking access to the server or client ports. + +If you have verified the above and still have issues, please +[open an issue](https://github.com/mongodb/docs-sample-apps/issues/new/choose) +on the source repository `mongodb/docs-sample-apps`. diff --git a/mflix/client/.gitignore b/mflix/client/.gitignore new file mode 100644 index 0000000..189ae56 --- /dev/null +++ b/mflix/client/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions +package-lock.json + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/mflix/client/app/aggregations/aggregations.module.css b/mflix/client/app/aggregations/aggregations.module.css new file mode 100644 index 0000000..89a3d13 --- /dev/null +++ b/mflix/client/app/aggregations/aggregations.module.css @@ -0,0 +1,172 @@ +/* Aggregations styles */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +.title { + font-size: 2.5rem; + font-weight: 700; + color: #1a1a1a; + margin-bottom: 0.5rem; + text-align: center; +} + +.subtitle { + font-size: 1.1rem; + color: #666; + text-align: center; + margin-bottom: 3rem; +} + +.section { + margin-bottom: 3rem; + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +.sectionTitle { + font-size: 1.5rem; + font-weight: 600; + color: #2c3e50; + margin: 0; + padding: 1.5rem 2rem; + background: #f8f9fa; + border-bottom: 1px solid #e9ecef; +} + +.tableContainer { + overflow-x: auto; + padding: 0; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; +} + +.table th { + background: #34495e; + color: white; + font-weight: 600; + padding: 1rem; + text-align: left; + white-space: nowrap; +} + +.table td { + padding: 1rem; + border-bottom: 1px solid #e9ecef; + vertical-align: top; +} + +.table tr:hover { + background: #f8f9fa; +} + +.movieTitle { + font-weight: 600; + color: #2c3e50; + max-width: 200px; + word-wrap: break-word; +} + +.year { + font-weight: 600; +} + +.rank { + font-weight: 700; +} + +.directorName { + font-weight: 600; + color: #2c3e50; +} + +.commentsContainer { + max-width: 300px; +} + +.comment { + margin-bottom: 0.75rem; + padding: 0.5rem; + background: #f8f9fa; + border-radius: 4px; + border-left: 3px solid #3498db; +} + +.comment:last-child { + margin-bottom: 0; +} + +.commentText { + font-size: 0.85rem; + color: #2c3e50; + margin-bottom: 0.25rem; + line-height: 1.4; +} + +.commentMeta { + font-size: 0.75rem; + color: #7f8c8d; + font-style: italic; +} + +.error { + color: #e74c3c; + background: #ffeaa7; + padding: 1rem; + margin: 1rem 2rem; + border-radius: 4px; + font-weight: 500; +} + +/* Responsive design */ +@media (max-width: 768px) { + .container { + padding: 1rem; + } + + .title { + font-size: 2rem; + } + + .sectionTitle { + padding: 1rem; + font-size: 1.25rem; + } + + .table { + font-size: 0.8rem; + } + + .table th, + .table td { + padding: 0.5rem; + } + + .commentsContainer { + max-width: 200px; + } + + .movieTitle { + max-width: 150px; + } +} + +@media (max-width: 480px) { + .table th, + .table td { + padding: 0.25rem; + } + + .commentsContainer { + max-width: 150px; + } +} \ No newline at end of file diff --git a/mflix/client/app/aggregations/page.tsx b/mflix/client/app/aggregations/page.tsx new file mode 100644 index 0000000..a211141 --- /dev/null +++ b/mflix/client/app/aggregations/page.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '../lib/api'; +import { MovieWithComments, YearlyStats, DirectorStats } from '../types/aggregations'; +import styles from './aggregations.module.css'; + +export default async function AggregationsPage() { + const MOVIES_WITH_COMMENTS_LIMIT = 5; + const DIRECTOR_STATS_LIMIT = 15; + + // Fetch all aggregation data with error handling + const [commentsResult, yearResult, directorsResult] = await Promise.allSettled([ + fetchMoviesWithComments(MOVIES_WITH_COMMENTS_LIMIT), + fetchMoviesByYear(), + fetchDirectorStats(DIRECTOR_STATS_LIMIT) + ]); + + // Process results with fallbacks + const commentsData = commentsResult.status === 'fulfilled' ? commentsResult.value : { success: false, error: 'Failed to fetch comments data' }; + const yearData = yearResult.status === 'fulfilled' ? yearResult.value : { success: false, error: 'Failed to fetch year data' }; + const directorsData = directorsResult.status === 'fulfilled' ? directorsResult.value : { success: false, error: 'Failed to fetch directors data' }; + + if (process.env.NODE_ENV === 'development') { + console.log('Aggregations SSR: Data fetch completed', { + comments: commentsData.success, + year: yearData.success, + directors: directorsData.success + }); + } + + return ( +
+

Movie Analytics Aggregations

+

+ Explore movie data through various aggregations and insights +

+ + {/* Movies with Recent Comments Section */} +
+

Movies with Recent Comments

+ {commentsData.success && commentsData.data ? ( +
+ + + + + + + + + + + + {(commentsData.data as MovieWithComments[]).map((movie) => ( + + + + + + + + ))} + +
Movie TitleYearRatingTotal CommentsRecent Comments
{movie.title}{movie.year}{movie.imdbRating ? movie.imdbRating.toFixed(1) : 'N/A'}{movie.totalComments} +
+ {movie.recentComments?.slice(0, 2).map((comment, index) => ( +
+
+ “{(comment.text || 'No text').slice(0, 80)}{comment.text?.length > 80 ? '...' : ''}” +
+
+ by {comment.userName} on {new Date(comment.date).toLocaleDateString()} +
+
+ )) ||
No recent comments
} +
+
+
+ ) : ( +
+ Failed to load movies with comments: {commentsData.error || 'Unknown error'} +
+ )} +
+ + {/* Movies by Year Section */} +
+

Movies by Year Statistics

+ {yearData.success && yearData.data ? ( +
+ + + + + + + + + + + + + {(yearData.data as YearlyStats[]).slice(0, 20).map((yearStats) => ( + + + + + + + + + ))} + +
YearMovie CountAverage RatingHighest RatingLowest RatingTotal Votes
{yearStats.year}{yearStats.movieCount}{yearStats.averageRating ? yearStats.averageRating.toFixed(2) : 'N/A'}{yearStats.highestRating ? yearStats.highestRating.toFixed(1) : 'N/A'}{yearStats.lowestRating ? yearStats.lowestRating.toFixed(1) : 'N/A'}{yearStats.totalVotes?.toLocaleString() || 'N/A'}
+
+ ) : ( +
+ Failed to load yearly statistics: {yearData.error || 'Unknown error'} +
+ )} +
+ + {/* Directors with Most Movies Section */} +
+

Directors with Most Movies

+ {directorsData.success && directorsData.data ? ( +
+ + + + + + + + + + + {(directorsData.data as DirectorStats[]).map((director, index) => ( + + + + + + + ))} + +
RankDirectorMovie CountAverage Rating
#{index + 1}{director.director}{director.movieCount}{director.averageRating ? director.averageRating.toFixed(2) : 'N/A'}
+
+ ) : ( +
+ Failed to load director statistics: {directorsData.error || 'Unknown error'} +
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/mflix/client/app/components/ActionButtons/ActionButtons.module.css b/mflix/client/app/components/ActionButtons/ActionButtons.module.css new file mode 100644 index 0000000..7372279 --- /dev/null +++ b/mflix/client/app/components/ActionButtons/ActionButtons.module.css @@ -0,0 +1,84 @@ +/** + * Action Buttons Styles + * + * CSS Module for the action buttons component. + * Provides consistent styling with the rest of the application. + */ + +.actionButtons { + display: flex; + gap: 1rem; + margin-bottom: 2rem; + justify-content: flex-start; +} + +.button { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; +} + +.button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.editButton { + background: #0070f3; + color: white; + border: 1px solid #0070f3; +} + +.editButton:hover:not(:disabled) { + background: #0051cc; + border-color: #0051cc; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 112, 243, 0.3); +} + +.deleteButton { + background: #dc2626; + color: white; + border: 1px solid #dc2626; +} + +.deleteButton:hover:not(:disabled) { + background: #b91c1c; + border-color: #b91c1c; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .actionButtons { + flex-direction: column; + gap: 0.75rem; + } + + .button { + width: 100%; + padding: 0.875rem 1rem; + } +} + +@media (max-width: 480px) { + .actionButtons { + gap: 0.5rem; + } + + .button { + padding: 0.75rem 1rem; + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/mflix/client/app/components/ActionButtons/ActionButtons.tsx b/mflix/client/app/components/ActionButtons/ActionButtons.tsx new file mode 100644 index 0000000..eae57b1 --- /dev/null +++ b/mflix/client/app/components/ActionButtons/ActionButtons.tsx @@ -0,0 +1,45 @@ +'use client'; + +/** + * Action Buttons Component + * + * Provides Edit and Delete actions for movie details page + */ + +import styles from './ActionButtons.module.css'; + +interface ActionButtonsProps { + onEdit: () => void; + onDelete: () => void; + isLoading?: boolean; + disabled?: boolean; +} + +export default function ActionButtons({ + onEdit, + onDelete, + isLoading = false, + disabled = false +}: ActionButtonsProps) { + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/mflix/client/app/components/ActionButtons/index.ts b/mflix/client/app/components/ActionButtons/index.ts new file mode 100644 index 0000000..eaca7eb --- /dev/null +++ b/mflix/client/app/components/ActionButtons/index.ts @@ -0,0 +1 @@ +export { default } from './ActionButtons'; \ No newline at end of file diff --git a/mflix/client/app/components/AddMovieForm/AddMovieForm.tsx b/mflix/client/app/components/AddMovieForm/AddMovieForm.tsx new file mode 100644 index 0000000..d040adf --- /dev/null +++ b/mflix/client/app/components/AddMovieForm/AddMovieForm.tsx @@ -0,0 +1,427 @@ +'use client'; + +/** + * Add Movie Form Component + * + * Form for creating new movies with validation. + * Supports both single movie creation and batch creation. + */ + +import { useState } from 'react'; +import { Movie } from '../../types/movie'; +import styles from '../EditMovieForm/EditMovieForm.module.css'; + +interface AddMovieFormProps { + onSave: (movieData: Omit[]) => void; + onCancel: () => void; + isLoading?: boolean; +} + +interface MovieFormData { + title: string; + year: string; + plot: string; + runtime: string; + rated: string; + genres: string; + directors: string; + writers: string; + cast: string; + countries: string; + languages: string; + poster: string; +} + +const getInitialFormData = (): MovieFormData => ({ + title: '', + year: '', + plot: '', + runtime: '', + rated: '', + genres: '', + directors: '', + writers: '', + cast: '', + countries: '', + languages: '', + poster: '', +}); + +export default function AddMovieForm({ + onSave, + onCancel, + isLoading = false +}: AddMovieFormProps) { + const [movieForms, setMovieForms] = useState([getInitialFormData()]); + const [errors, setErrors] = useState>>({}); + + const validateForm = (formData: MovieFormData, index: number) => { + const newErrors: Record = {}; + + if (!formData.title.trim()) { + newErrors.title = 'Title is required'; + } + + if (formData.year && (parseInt(formData.year) < 1800 || parseInt(formData.year) > new Date().getFullYear() + 5)) { + newErrors.year = 'Please enter a valid year'; + } + + if (formData.runtime && (parseInt(formData.runtime) < 1 || parseInt(formData.runtime) > 1000)) { + newErrors.runtime = 'Please enter a valid runtime in minutes'; + } + + return newErrors; + }; + + const validateAllForms = () => { + const allErrors: Record> = {}; + let hasErrors = false; + + movieForms.forEach((formData, index) => { + const formErrors = validateForm(formData, index); + if (Object.keys(formErrors).length > 0) { + allErrors[index] = formErrors; + hasErrors = true; + } + }); + + setErrors(allErrors); + return !hasErrors; + }; + + const convertFormDataToMovie = (formData: MovieFormData): Omit => { + const movieData: Omit = { + title: formData.title.trim(), + year: formData.year ? parseInt(formData.year) : undefined, + plot: formData.plot.trim() || undefined, + runtime: formData.runtime ? parseInt(formData.runtime) : undefined, + rated: formData.rated.trim() || undefined, + genres: formData.genres ? formData.genres.split(',').map(g => g.trim()).filter(g => g) : undefined, + directors: formData.directors ? formData.directors.split(',').map(d => d.trim()).filter(d => d) : undefined, + writers: formData.writers ? formData.writers.split(',').map(w => w.trim()).filter(w => w) : undefined, + cast: formData.cast ? formData.cast.split(',').map(c => c.trim()).filter(c => c) : undefined, + countries: formData.countries ? formData.countries.split(',').map(c => c.trim()).filter(c => c) : undefined, + languages: formData.languages ? formData.languages.split(',').map(l => l.trim()).filter(l => l) : undefined, + poster: formData.poster.trim() || undefined, + }; + + // Remove undefined values + return Object.fromEntries( + Object.entries(movieData).filter(([_, value]) => value !== undefined) + ) as Omit; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateAllForms()) { + return; + } + + // Convert all form data to movie objects + const moviesData = movieForms.map(convertFormDataToMovie); + onSave(moviesData); + }; + + const handleInputChange = (index: number, field: string, value: string) => { + setMovieForms(prev => prev.map((form, i) => + i === index ? { ...form, [field]: value } : form + )); + + // Clear error when user starts typing + if (errors[index]?.[field]) { + setErrors(prev => ({ + ...prev, + [index]: { ...prev[index], [field]: '' } + })); + } + }; + + const handleAddMore = () => { + setMovieForms(prev => [...prev, getInitialFormData()]); + }; + + const handleRemoveForm = (index: number) => { + if (movieForms.length > 1) { + setMovieForms(prev => prev.filter((_, i) => i !== index)); + setErrors(prev => { + const newErrors = { ...prev }; + delete newErrors[index]; + // Reindex errors for forms after the removed one + Object.keys(newErrors).forEach(key => { + const keyNum = parseInt(key); + if (keyNum > index) { + newErrors[keyNum - 1] = newErrors[keyNum]; + delete newErrors[keyNum]; + } + }); + return newErrors; + }); + } + }; + + const renderMovieForm = (formData: MovieFormData, index: number) => { + const formErrors = errors[index] || {}; + + return ( +
+ {movieForms.length > 1 && ( +
+

Movie {index + 1}

+ +
+ )} + +
+ {/* Title */} +
+ + handleInputChange(index, 'title', e.target.value)} + className={`${styles.input} ${formErrors.title ? styles.inputError : ''}`} + disabled={isLoading} + required + /> + {formErrors.title && {formErrors.title}} +
+ + {/* Year */} +
+ + handleInputChange(index, 'year', e.target.value)} + className={`${styles.input} ${formErrors.year ? styles.inputError : ''}`} + disabled={isLoading} + min="1800" + max={new Date().getFullYear() + 5} + /> + {formErrors.year && {formErrors.year}} +
+ + {/* Runtime */} +
+ + handleInputChange(index, 'runtime', e.target.value)} + className={`${styles.input} ${formErrors.runtime ? styles.inputError : ''}`} + disabled={isLoading} + min="1" + max="1000" + /> + {formErrors.runtime && {formErrors.runtime}} +
+ + {/* Rated */} +
+ + handleInputChange(index, 'rated', e.target.value)} + className={styles.input} + disabled={isLoading} + placeholder="e.g., PG-13, R, G" + /> +
+ + {/* Poster URL */} +
+ + handleInputChange(index, 'poster', e.target.value)} + className={styles.input} + disabled={isLoading} + placeholder="https://..." + /> +
+
+ + {/* Plot */} +
+ +