diff --git a/.github/scripts/generate-test-summary-jest.sh b/.github/scripts/generate-test-summary-jest.sh index 48e86bf..cb40ff7 100644 --- a/.github/scripts/generate-test-summary-jest.sh +++ b/.github/scripts/generate-test-summary-jest.sh @@ -5,11 +5,17 @@ set -e # Shows breakdown by test type (unit vs integration) # Usage: ./generate-test-summary-jest.sh +# Guard: skip if GITHUB_STEP_SUMMARY is not set +if [ -z "$GITHUB_STEP_SUMMARY" ]; then + echo "Warning: GITHUB_STEP_SUMMARY not set, skipping summary generation" + exit 0 +fi + UNIT_JSON="${1:-}" INTEGRATION_JSON="${2:-}" -echo "## Test Results" >> $GITHUB_STEP_SUMMARY -echo "" >> $GITHUB_STEP_SUMMARY +echo "## Test Results" >> "$GITHUB_STEP_SUMMARY" +echo "" >> "$GITHUB_STEP_SUMMARY" # Function to parse Jest JSON file parse_json() { @@ -55,37 +61,37 @@ 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 +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 + 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 + 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 +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 +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 + echo "### ❌ Failed Tests" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" failed_tests_file=$(mktemp) @@ -107,16 +113,16 @@ if [ $total_failed -gt 0 ]; then if [ -s "$failed_tests_file" ]; then while IFS= read -r test; do - echo "- \`$test\`" >> $GITHUB_STEP_SUMMARY + echo "- \`$test\`" >> "$GITHUB_STEP_SUMMARY" done < "$failed_tests_file" else - echo "_Unable to parse individual test names_" >> $GITHUB_STEP_SUMMARY + echo "_Unable to parse individual test names_" >> "$GITHUB_STEP_SUMMARY" fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "❌ **Tests failed!**" >> $GITHUB_STEP_SUMMARY + 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 + echo "✅ **All tests passed!**" >> "$GITHUB_STEP_SUMMARY" fi diff --git a/.github/workflows/run-express-tests.yml b/.github/workflows/run-express-tests.yml index d434da2..5f2fdd4 100644 --- a/.github/workflows/run-express-tests.yml +++ b/.github/workflows/run-express-tests.yml @@ -41,6 +41,16 @@ jobs: - name: Add sample data to database run: mongorestore --archive=sampledata.archive --port=27017 + - name: Install MongoDB Shell (mongosh) + run: | + curl https://downloads.mongodb.com/compass/mongosh-2.3.8-linux-x64.tgz -o mongosh.tgz + tar -xzf mongosh.tgz + sudo cp mongosh-2.3.8-linux-x64/bin/* /usr/local/bin/ + + - name: Create indexes for aggregation performance + run: | + mongosh "mongodb://localhost:27017/sample_mflix?directConnection=true" --eval "db.comments.createIndex({ movie_id: 1 })" + - name: Set up Node.js uses: actions/setup-node@v4 with: @@ -52,15 +62,16 @@ jobs: - name: Run unit tests working-directory: mflix/server/js-express - run: npm run test:unit -- --json --outputFile=test-results-unit.json || true + run: npm run test:unit -- --json --outputFile=test-results-unit.json env: - MONGODB_URI: mongodb://localhost:27017/sample_mflix + MONGODB_URI: mongodb://localhost:27017/sample_mflix?directConnection=true - name: Run integration tests working-directory: mflix/server/js-express - run: npm run test:integration -- --json --outputFile=test-results-integration.json || true + continue-on-error: true + run: npm run test:integration -- --json --outputFile=test-results-integration.json env: - MONGODB_URI: mongodb://localhost:27017/sample_mflix + MONGODB_URI: mongodb://localhost:27017/sample_mflix?directConnection=true ENABLE_SEARCH_TESTS: true # Note: Vector search tests will be skipped without VOYAGE_API_KEY # Run these tests locally with a valid API key diff --git a/mflix/README-JAVA-SPRING.md b/mflix/README-JAVA-SPRING.md index d73a717..245afc5 100644 --- a/mflix/README-JAVA-SPRING.md +++ b/mflix/README-JAVA-SPRING.md @@ -14,12 +14,16 @@ This is a full-stack movie browsing application built with Java Spring Boot and └── mvnw ``` +## Data Limitations + +The `sample_mflix` dataset contains movies released up to **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application. + ## 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/) + - [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/) diff --git a/mflix/README-JAVASCRIPT-EXPRESS.md b/mflix/README-JAVASCRIPT-EXPRESS.md index 130bf5f..f3c531c 100644 --- a/mflix/README-JAVASCRIPT-EXPRESS.md +++ b/mflix/README-JAVASCRIPT-EXPRESS.md @@ -14,6 +14,10 @@ This is a full-stack movie browsing application built with Express.js and Next.j └── tsconfig.json ``` +## Data Limitations + +The `sample_mflix` dataset contains movies released up to **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application. + ## Prerequisites - **Node.js 22** or higher diff --git a/mflix/README-PYTHON-FASTAPI.md b/mflix/README-PYTHON-FASTAPI.md index f3be9de..3d79f3b 100644 --- a/mflix/README-PYTHON-FASTAPI.md +++ b/mflix/README-PYTHON-FASTAPI.md @@ -17,6 +17,10 @@ This is a full-stack movie browsing application built with Python FastAPI and Ne └── requirements.txt ``` +## Data Limitations + +The `sample_mflix` dataset contains movies released up to **2016**. Searching for movies from 2017 or later will return no results. This is a limitation of the sample dataset, not the application. + ## Prerequisites - **Python 3.10** to **Python 3.13** diff --git a/mflix/client/app/aggregations/aggregations.module.css b/mflix/client/app/aggregations/aggregations.module.css index 89a3d13..6296b1b 100644 --- a/mflix/client/app/aggregations/aggregations.module.css +++ b/mflix/client/app/aggregations/aggregations.module.css @@ -1,42 +1,53 @@ -/* Aggregations styles */ +/* Aggregations styles - MongoDB Branded */ .container { max-width: 1200px; margin: 0 auto; - padding: 2rem; + padding: 2.5rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; } .title { - font-size: 2.5rem; + font-size: 2.75rem; font-weight: 700; - color: #1a1a1a; - margin-bottom: 0.5rem; + color: var(--mongodb-slate); + margin-bottom: 0.75rem; text-align: center; } +.title::after { + content: ''; + display: block; + width: 100px; + height: 4px; + background: var(--mongodb-spring); + margin: 1rem auto 0; + border-radius: 2px; +} + .subtitle { - font-size: 1.1rem; - color: #666; + font-size: 1.15rem; + color: var(--color-text-secondary); text-align: center; margin-bottom: 3rem; + font-weight: 500; } .section { margin-bottom: 3rem; - background: #fff; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background: var(--mongodb-white); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); overflow: hidden; + border: 2px solid var(--mongodb-mint); } .sectionTitle { font-size: 1.5rem; - font-weight: 600; - color: #2c3e50; + font-weight: 700; + color: var(--mongodb-white); margin: 0; padding: 1.5rem 2rem; - background: #f8f9fa; - border-bottom: 1px solid #e9ecef; + background: var(--mongodb-forest); } .tableContainer { @@ -47,31 +58,34 @@ .table { width: 100%; border-collapse: collapse; - font-size: 0.9rem; + font-size: 0.95rem; } .table th { - background: #34495e; - color: white; + background: var(--mongodb-slate); + color: var(--mongodb-white); font-weight: 600; - padding: 1rem; + padding: 1.125rem 1.25rem; text-align: left; white-space: nowrap; + font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.05em; } .table td { - padding: 1rem; - border-bottom: 1px solid #e9ecef; + padding: 1.125rem 1.25rem; + border-bottom: 1px solid var(--color-border); vertical-align: top; } .table tr:hover { - background: #f8f9fa; + background: #F7FAFC; } .movieTitle { font-weight: 600; - color: #2c3e50; + color: var(--mongodb-slate); max-width: 200px; word-wrap: break-word; } @@ -96,9 +110,9 @@ .comment { margin-bottom: 0.75rem; padding: 0.5rem; - background: #f8f9fa; - border-radius: 4px; - border-left: 3px solid #3498db; + background: #F7FAFC; + border-radius: var(--radius-md); + border-left: 2px solid var(--color-border); } .comment:last-child { @@ -107,14 +121,14 @@ .commentText { font-size: 0.85rem; - color: #2c3e50; + color: var(--mongodb-slate); margin-bottom: 0.25rem; line-height: 1.4; } .commentMeta { font-size: 0.75rem; - color: #7f8c8d; + color: var(--color-text-muted); font-style: italic; } diff --git a/mflix/client/app/aggregations/page.tsx b/mflix/client/app/aggregations/page.tsx index 00b8d16..d7766b6 100644 --- a/mflix/client/app/aggregations/page.tsx +++ b/mflix/client/app/aggregations/page.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { fetchMoviesWithComments, fetchMoviesByYear, fetchDirectorStats } from '@/lib/api'; import { MovieWithComments, YearlyStats, DirectorStats } from '@/types/aggregations'; import styles from './aggregations.module.css'; +import ExpandableTable from '@/components/ExpandableTable/ExpandableTable'; export default async function AggregationsPage() { const MOVIES_WITH_COMMENTS_LIMIT = 5; @@ -38,43 +39,48 @@ export default async function AggregationsPage() {

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
} -
-
+ + + + + + + - ))} - -
Movie TitleYearRatingTotal CommentsRecent Comments
-
+ + + {(commentsData.data as MovieWithComments[]).map((movie) => ( + + {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'} @@ -86,32 +92,37 @@ export default async function AggregationsPage() {

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'}
+ + + + + + + + - ))} - -
YearMovie CountAverage RatingHighest RatingLowest RatingTotal Votes
-
+ + + {(yearData.data as YearlyStats[]).map((yearStats) => ( + + {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'} @@ -123,28 +134,33 @@ export default async function AggregationsPage() {

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'}
+ + + + + + - ))} - -
RankDirectorMovie CountAverage Rating
-
+ + + {(directorsData.data as DirectorStats[]).map((director, index) => ( + + #{index + 1} + {director.director} + {director.movieCount} + {director.averageRating ? director.averageRating.toFixed(2) : 'N/A'} + + ))} + + +
+ ) : (
Failed to load director statistics: {directorsData.error || 'Unknown error'} diff --git a/mflix/client/app/components/ActionButtons/ActionButtons.module.css b/mflix/client/app/components/ActionButtons/ActionButtons.module.css index 7372279..a5c1b32 100644 --- a/mflix/client/app/components/ActionButtons/ActionButtons.module.css +++ b/mflix/client/app/components/ActionButtons/ActionButtons.module.css @@ -1,6 +1,6 @@ /** - * Action Buttons Styles - * + * Action Buttons Styles - MongoDB Branded + * * CSS Module for the action buttons component. * Provides consistent styling with the rest of the application. */ @@ -10,53 +10,67 @@ gap: 1rem; margin-bottom: 2rem; justify-content: flex-start; + flex-wrap: wrap; } .button { - padding: 0.75rem 1.5rem; + padding: 0.875rem 1.75rem; border: none; - border-radius: 8px; + border-radius: var(--radius-lg); font-size: 1rem; - font-weight: 500; + font-weight: 600; cursor: pointer; - transition: all 0.2s ease; + transition: all var(--transition-base); text-decoration: none; display: inline-flex; align-items: center; justify-content: center; - min-width: 120px; + min-width: 140px; + box-shadow: var(--shadow-sm); } .button:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; transform: none; + box-shadow: none; } .editButton { - background: #0070f3; - color: white; - border: 1px solid #0070f3; + background: var(--mongodb-forest); + color: var(--mongodb-white); + border: 2px solid var(--mongodb-forest); } .editButton:hover:not(:disabled) { - background: #0051cc; - border-color: #0051cc; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 112, 243, 0.3); + background: var(--mongodb-evergreen); + border-color: var(--mongodb-evergreen); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 104, 74, 0.4); +} + +.editButton:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--shadow-sm); } .deleteButton { - background: #dc2626; - color: white; - border: 1px solid #dc2626; + background: var(--mongodb-white); + color: var(--color-error); + border: 2px solid var(--color-error); } .deleteButton:hover:not(:disabled) { - background: #b91c1c; - border-color: #b91c1c; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(220, 38, 38, 0.3); + background: var(--color-error); + color: var(--mongodb-white); + border-color: var(--color-error); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(229, 62, 62, 0.4); +} + +.deleteButton:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--shadow-sm); } /* Responsive Design */ @@ -68,17 +82,18 @@ .button { width: 100%; - padding: 0.875rem 1rem; + padding: 1rem; } } @media (max-width: 480px) { .actionButtons { - gap: 0.5rem; + gap: 0.625rem; } .button { - padding: 0.75rem 1rem; - font-size: 0.9rem; + padding: 0.875rem 1rem; + font-size: 0.95rem; + min-width: 120px; } } \ No newline at end of file diff --git a/mflix/client/app/components/EditMovieForm/EditMovieForm.module.css b/mflix/client/app/components/EditMovieForm/EditMovieForm.module.css index c5887d8..def3a42 100644 --- a/mflix/client/app/components/EditMovieForm/EditMovieForm.module.css +++ b/mflix/client/app/components/EditMovieForm/EditMovieForm.module.css @@ -1,47 +1,60 @@ /** - * Edit Movie Form Styles - * + * Edit Movie Form Styles - MongoDB Branded + * * CSS Module for the edit movie form component. * Provides consistent styling with the rest of the application. */ .formContainer { - background: white; - border-radius: 12px; - padding: 2rem; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + background: var(--mongodb-white); + border-radius: var(--radius-xl); + padding: 2.5rem; + box-shadow: var(--shadow-lg); margin-bottom: 2rem; + border: 2px solid var(--mongodb-mint); } .formTitle { - font-size: 1.75rem; - font-weight: bold; - color: #333; + font-size: 2rem; + font-weight: 700; + color: var(--mongodb-slate); margin: 0 0 1.5rem 0; text-align: center; } +.formTitle::after { + content: ''; + display: block; + width: 60px; + height: 3px; + background: var(--mongodb-spring); + margin: 0.75rem auto 0; + border-radius: 2px; +} + .batchDescription { - background-color: #f8f9fa; - border: 1px solid #e9ecef; - border-radius: 8px; - padding: 1rem; + background: var(--mongodb-mint); + border: 2px solid var(--mongodb-forest); + border-radius: var(--radius-lg); + padding: 1.25rem; margin-bottom: 1.5rem; - color: #495057; - font-size: 0.9rem; - line-height: 1.4; + color: var(--mongodb-slate); + font-size: 0.95rem; + line-height: 1.5; text-align: center; + font-weight: 500; } .generalError { - background-color: #f8d7da; - border: 1px solid #f5c6cb; - color: #721c24; - padding: 0.75rem 1rem; - border-radius: 8px; - margin-bottom: 1rem; - font-size: 0.9rem; + background: #fef2f2; + border: 2px solid var(--color-error); + color: #991b1b; + padding: 1rem 1.25rem; + border-radius: var(--radius-lg); + margin-bottom: 1.5rem; + font-size: 0.95rem; text-align: center; + font-weight: 600; } .form { @@ -58,57 +71,59 @@ .formGroup { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.625rem; } .label { - font-weight: 500; - color: #333; - font-size: 0.9rem; + font-weight: 600; + color: var(--mongodb-slate); + font-size: 0.95rem; } .input, .textarea { - padding: 0.75rem; - border: 2px solid #e1e5e9; - border-radius: 8px; + padding: 0.875rem; + border: 2px solid var(--color-border); + border-radius: var(--radius-md); font-size: 1rem; - transition: border-color 0.2s ease, box-shadow 0.2s ease; - background: white; + transition: all var(--transition-base); + background: var(--mongodb-white); + color: var(--mongodb-slate); } .input:focus, .textarea:focus { outline: none; - border-color: #0070f3; - box-shadow: 0 0 0 3px rgba(0, 112, 243, 0.1); + border-color: var(--mongodb-forest); + box-shadow: 0 0 0 3px rgba(0, 104, 74, 0.1); } .input:disabled, .textarea:disabled { - background: #f8f9fa; - color: #6c757d; + background: var(--color-border); + color: var(--color-text-muted); cursor: not-allowed; } .inputError { - border-color: #dc2626 !important; + border-color: var(--color-error) !important; } .inputError:focus { - box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1) !important; + box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.1) !important; } .textarea { resize: vertical; - min-height: 100px; + min-height: 120px; font-family: inherit; } .error { - color: #dc2626; + color: var(--color-error); font-size: 0.875rem; - margin-top: 0.25rem; + margin-top: 0.375rem; + font-weight: 500; } .listFields { @@ -122,51 +137,54 @@ display: flex; gap: 1rem; justify-content: flex-end; - padding-top: 1.5rem; - border-top: 1px solid #e1e5e9; + padding-top: 2rem; + border-top: 2px solid var(--mongodb-mint); + margin-top: 1rem; } .button { - padding: 0.75rem 1.5rem; + padding: 0.875rem 1.75rem; border: none; - border-radius: 8px; + border-radius: var(--radius-lg); font-size: 1rem; - font-weight: 500; + font-weight: 600; cursor: pointer; - transition: all 0.2s ease; - min-width: 120px; + transition: all var(--transition-base); + min-width: 140px; + box-shadow: var(--shadow-sm); } .button:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; transform: none; + box-shadow: none; } .saveButton { - background: #0070f3; - color: white; - border: 1px solid #0070f3; + background: var(--mongodb-forest); + color: var(--mongodb-white); + border: 2px solid var(--mongodb-forest); } .saveButton:hover:not(:disabled) { - background: #0051cc; - border-color: #0051cc; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 112, 243, 0.3); + background: var(--mongodb-evergreen); + border-color: var(--mongodb-evergreen); + transform: translateY(-2px); + box-shadow: 0 6px 16px rgba(0, 104, 74, 0.4); } .cancelButton { - background: #6c757d; - color: white; - border: 1px solid #6c757d; + background: var(--mongodb-white); + color: var(--color-text-secondary); + border: 2px solid var(--color-border); } .cancelButton:hover:not(:disabled) { - background: #5a6268; - border-color: #5a6268; - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(108, 117, 125, 0.3); + background: var(--color-border); + border-color: var(--color-text-secondary); + transform: translateY(-2px); + box-shadow: var(--shadow-md); } /* Multi-movie form styles */ diff --git a/mflix/client/app/components/ExpandableTable/ExpandableTable.module.css b/mflix/client/app/components/ExpandableTable/ExpandableTable.module.css new file mode 100644 index 0000000..04c6cdb --- /dev/null +++ b/mflix/client/app/components/ExpandableTable/ExpandableTable.module.css @@ -0,0 +1,109 @@ +.container { + position: relative; +} + +.tableWrapper { + position: relative; + overflow: hidden; + transition: max-height 0.3s ease-in-out; +} + +.collapsed { + max-height: 600px; +} + +.expanded { + max-height: none; +} + +.collapsed::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 80px; + background: linear-gradient(to bottom, transparent, var(--mongodb-white)); + pointer-events: none; +} + +.buttonContainer { + display: flex; + justify-content: center; + padding: 1.5rem 0; + margin-top: 1rem; +} + +.expandButton { + display: inline-flex; + align-items: center; + gap: 0.625rem; + padding: 0.625rem 1.25rem; + background: transparent; + color: var(--color-text-secondary); + border: none; + border-radius: 50px; + font-size: 0.9375rem; + font-weight: 500; + letter-spacing: 0.01em; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} + +.expandButton:hover { + color: var(--mongodb-forest); + background: #F7FAFC; +} + +.expandButton:active { + transform: translateY(0); +} + +.expandButton:active::before { + box-shadow: 0 4px 8px rgba(0, 104, 74, 0.15); +} + +.expandButton:focus { + outline: none; +} + +.expandButton:focus-visible::after { + content: ''; + position: absolute; + inset: -3px; + border: 2px solid var(--mongodb-spring); + border-radius: 50px; + animation: focusPulse 1.5s ease-in-out infinite; +} + +@keyframes focusPulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + +.buttonIcon { + font-size: 0.75rem; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + display: inline-block; +} + +.expandButton:hover .buttonIcon { + transform: translateY(2px); +} + +@media (max-width: 768px) { + .collapsed { + max-height: 400px; + } + + .expandButton { + font-size: 0.875rem; + padding: 0.625rem 1.25rem; + } +} + diff --git a/mflix/client/app/components/ExpandableTable/ExpandableTable.tsx b/mflix/client/app/components/ExpandableTable/ExpandableTable.tsx new file mode 100644 index 0000000..abc1cf1 --- /dev/null +++ b/mflix/client/app/components/ExpandableTable/ExpandableTable.tsx @@ -0,0 +1,50 @@ +'use client'; + +import React, { useState } from 'react'; +import styles from './ExpandableTable.module.css'; + +interface ExpandableTableProps { + children: React.ReactNode; + initialRowCount?: number; + totalRowCount: number; +} + +export default function ExpandableTable({ + children, + initialRowCount = 10, + totalRowCount +}: ExpandableTableProps) { + const [isExpanded, setIsExpanded] = useState(false); + const showExpandButton = totalRowCount > initialRowCount; + + return ( +
+
+ {children} +
+ {showExpandButton && ( +
+ +
+ )} +
+ ); +} + diff --git a/mflix/client/app/components/FilterBar/FilterBar.module.css b/mflix/client/app/components/FilterBar/FilterBar.module.css index ee17f25..91bfeb2 100644 --- a/mflix/client/app/components/FilterBar/FilterBar.module.css +++ b/mflix/client/app/components/FilterBar/FilterBar.module.css @@ -1,29 +1,30 @@ /** - * FilterBar Component Styles + * FilterBar Component Styles - MongoDB Branded * * CSS Module for the movie filter bar component. * Provides a horizontal filter bar for filtering movies by genre, year, rating, etc. */ .filterBar { - background: white; - border-radius: 12px; - padding: 1.25rem 1.5rem; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - margin-bottom: 1.5rem; + background: var(--mongodb-white); + border-radius: var(--radius-xl); + padding: 1.5rem 2rem; + box-shadow: var(--shadow-md); + margin-bottom: 2rem; + border: 2px solid var(--color-border); } .filterHeader { display: flex; align-items: center; justify-content: space-between; - margin-bottom: 1rem; + margin-bottom: 1.25rem; } .filterTitle { - font-size: 0.875rem; - font-weight: 600; - color: #64748b; + font-size: 0.95rem; + font-weight: 700; + color: var(--mongodb-slate); text-transform: uppercase; letter-spacing: 0.05em; margin: 0; @@ -33,113 +34,134 @@ } .clearFiltersButton { - background: transparent; - border: 1px solid #e2e8f0; - color: #64748b; - padding: 0.375rem 0.75rem; - border-radius: 6px; - font-size: 0.8rem; + background: var(--mongodb-white); + border: 2px solid var(--mongodb-forest); + color: var(--mongodb-forest); + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + font-size: 0.85rem; + font-weight: 600; cursor: pointer; - transition: all 0.2s ease; + transition: all var(--transition-base); } .clearFiltersButton:hover { - background: #f8fafc; - border-color: #cbd5e1; - color: #475569; + background: #F7FAFC; + border-color: var(--color-text-secondary); + color: var(--color-text-secondary); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); } .filterControls { display: flex; flex-wrap: wrap; - gap: 1rem; + gap: 1.25rem; align-items: flex-end; } .filterGroup { display: flex; flex-direction: column; - gap: 0.25rem; - min-width: 140px; + gap: 0.5rem; + min-width: 150px; } .filterLabel { - font-size: 0.75rem; - font-weight: 500; - color: #64748b; + font-size: 0.85rem; + font-weight: 600; + color: var(--mongodb-slate); } .filterSelect, .filterInput { - padding: 0.5rem 0.75rem; - border: 2px solid #e2e8f0; - border-radius: 8px; - font-size: 0.875rem; - background: white; - color: #374151; - transition: border-color 0.2s ease, box-shadow 0.2s ease; - min-width: 120px; + padding: 0.625rem 0.875rem; + border: 2px solid var(--color-border); + border-radius: var(--radius-md); + font-size: 0.9rem; + background: var(--mongodb-white); + color: var(--mongodb-slate); + transition: all var(--transition-base); + min-width: 130px; } .filterSelect:hover, .filterInput:hover { - border-color: #cbd5e1; + border-color: var(--color-text-secondary); } .filterSelect:focus, .filterInput:focus { outline: none; - border-color: #3b82f6; - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15); + border-color: var(--color-text-secondary); + box-shadow: 0 0 0 3px rgba(74, 85, 104, 0.1); +} + +.inputWarning { + border-color: #f59e0b; +} + +.inputWarning:focus { + border-color: #f59e0b; + box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.15); +} + +.yearWarning { + font-size: 0.7rem; + color: #b45309; + margin-top: 0.25rem; } .ratingGroup { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.625rem; } .ratingInput { - width: 70px; + width: 80px; } .ratingDivider { - color: #94a3b8; - font-size: 0.875rem; + color: var(--color-text-muted); + font-size: 0.9rem; + font-weight: 600; } .applyButton { - background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); - color: white; + background: var(--mongodb-forest); + color: var(--mongodb-white); border: none; - padding: 0.5rem 1rem; - border-radius: 8px; - font-size: 0.875rem; + padding: 0.625rem 1.25rem; + border-radius: var(--radius-md); + font-size: 0.9rem; font-weight: 600; cursor: pointer; - transition: all 0.2s ease; + transition: all var(--transition-base); white-space: nowrap; + box-shadow: var(--shadow-sm); } .applyButton:hover { - background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); + background: var(--mongodb-evergreen); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 104, 74, 0.4); } .applyButton:disabled { opacity: 0.5; cursor: not-allowed; transform: none; + box-shadow: none; } .activeFilters { display: flex; flex-wrap: wrap; - gap: 0.5rem; - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid #e2e8f0; + gap: 0.625rem; + margin-top: 1.25rem; + padding-top: 1.25rem; + border-top: 2px solid var(--color-border); } .filterChip { diff --git a/mflix/client/app/components/FilterBar/FilterBar.tsx b/mflix/client/app/components/FilterBar/FilterBar.tsx index 324840d..8f6b45d 100644 --- a/mflix/client/app/components/FilterBar/FilterBar.tsx +++ b/mflix/client/app/components/FilterBar/FilterBar.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react'; import styles from './FilterBar.module.css'; -import { fetchGenres, type MovieFilterParams } from '@/lib/api'; +import { fetchGenres, fetchYearBounds, type MovieFilterParams } from '@/lib/api'; const SORT_OPTIONS = [ { value: 'title', label: 'Title' }, @@ -39,6 +39,8 @@ export default function FilterBar({ const [filters, setFilters] = useState(initialFilters); const [genres, setGenres] = useState([]); const [isLoadingGenres, setIsLoadingGenres] = useState(true); + const [maxDatasetYear, setMaxDatasetYear] = useState(null); + const [minDatasetYear, setMinDatasetYear] = useState(null); // Track previous initialFilters to detect changes const prevInitialFiltersRef = useRef(initialFilters); @@ -54,6 +56,28 @@ export default function FilterBar({ loadGenres(); }, []); + // Fetch year bounds from the API on mount + useEffect(() => { + async function loadYearBounds() { + console.log('FilterBar: Fetching year bounds...'); + const result = await fetchYearBounds(); + console.log('FilterBar: Year bounds result:', result); + if (result.success) { + if (result.maxYear) { + console.log('FilterBar: Setting maxDatasetYear to', result.maxYear); + setMaxDatasetYear(result.maxYear); + } + if (result.minYear) { + console.log('FilterBar: Setting minDatasetYear to', result.minYear); + setMinDatasetYear(result.minYear); + } + } else { + console.warn('FilterBar: Failed to fetch year bounds:', result.error); + } + } + loadYearBounds(); + }, []); + // Sync internal state when initialFilters changes (e.g. from URL navigation) useEffect(() => { if (!areFiltersEqual(prevInitialFiltersRef.current, initialFilters)) { @@ -134,16 +158,26 @@ export default function FilterBar({
+ {maxDatasetYear && filters.year && filters.year > maxDatasetYear && ( + + Dataset only contains movies up to {maxDatasetYear} + + )} + {minDatasetYear && filters.year && filters.year < minDatasetYear && ( + + Dataset only contains movies from {minDatasetYear} onwards + + )} maxDatasetYear) || (minDatasetYear && filters.year && filters.year < minDatasetYear) ? styles.inputWarning : ''}`} + placeholder="e.g. 2010" value={filters.year || ''} onChange={(e) => handleFilterChange('year', e.target.value ? parseInt(e.target.value) : undefined)} disabled={isLoading} - min={1900} - max={2030} + min={minDatasetYear || undefined} + max={maxDatasetYear || undefined} />
diff --git a/mflix/client/app/components/LoadingSkeleton/LoadingSkeleton.module.css b/mflix/client/app/components/LoadingSkeleton/LoadingSkeleton.module.css index 9fd700e..4a04d44 100644 --- a/mflix/client/app/components/LoadingSkeleton/LoadingSkeleton.module.css +++ b/mflix/client/app/components/LoadingSkeleton/LoadingSkeleton.module.css @@ -1,85 +1,77 @@ /** - * Loading Skeleton Styles - * + * Loading Skeleton Styles - MongoDB Branded + * * Reusable skeleton loading animations and styles */ /* Base skeleton element */ .skeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 4px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-sm); } /* Loading animation */ -@keyframes loading { - 0% { - background-position: 200% 0; +@keyframes pulse { + 0%, 100% { + opacity: 1; } - 100% { - background-position: -200% 0; + 50% { + opacity: 0.6; } } /* Skeleton variants */ .skeletonText { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 4px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-sm); height: 16px; margin-bottom: 0.5rem; } .skeletonTitle { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 4px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-sm); height: 24px; margin-bottom: 1rem; } .skeletonLargeTitle { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 4px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-sm); height: 40px; margin-bottom: 1rem; } .skeletonButton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 6px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-md); height: 40px; } .skeletonCard { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 8px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-lg); height: 300px; } .skeletonAvatar { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; border-radius: 50%; width: 40px; height: 40px; } .skeletonInput { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 6px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-md); height: 38px; } @@ -130,20 +122,19 @@ /* Movie-specific skeletons */ .movieCardSkeleton { - border: 1px solid #eee; - border-radius: 8px; + border: 2px solid var(--color-border); + border-radius: var(--radius-lg); padding: 16px; - background: white; + background: var(--mongodb-white); display: flex; flex-direction: column; gap: 16px; } .posterSkeleton { - background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 4px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-md); width: 100%; height: 300px; } @@ -180,13 +171,12 @@ /* Rating card skeleton */ .ratingCardSkeleton { - background: linear-gradient(90deg, #f8f8f8 25%, #e9ecef 50%, #f8f8f8 75%); - background-size: 200% 100%; - animation: loading 1.5s infinite; - border-radius: 8px; + background: var(--mongodb-mint); + animation: pulse 1.5s ease-in-out infinite; + border-radius: var(--radius-lg); padding: 1rem; text-align: center; - border: 1px solid #e9ecef; + border: 2px solid var(--color-border); display: flex; flex-direction: column; gap: 0.5rem; diff --git a/mflix/client/app/components/MovieCard/MovieCard.module.css b/mflix/client/app/components/MovieCard/MovieCard.module.css index c19765d..2baa7c2 100644 --- a/mflix/client/app/components/MovieCard/MovieCard.module.css +++ b/mflix/client/app/components/MovieCard/MovieCard.module.css @@ -1,6 +1,6 @@ /** - * Movies Card Styles - * + * Movies Card Styles - MongoDB Branded + * * CSS Module for the movies card component. * Provides responsive grid layout and movie card styling. */ @@ -14,43 +14,41 @@ } .movieCard { - border: 1px solid #ddd; - border-radius: 8px; + border: 2px solid var(--color-border); + border-radius: var(--radius-lg); padding: 16px; - background: white; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + background: var(--mongodb-white); + box-shadow: var(--shadow-sm); + transition: all var(--transition-base); position: relative; + overflow: hidden; + /* Enforce consistent card heights using flexbox */ + display: flex; + flex-direction: column; } -.movieCard:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +/* Make card clickable when in selection mode */ +.movieCard.selectable { + cursor: pointer; } -.movieCard.selected { - border-color: #0070f3; - box-shadow: 0 2px 4px rgba(0, 112, 243, 0.2); +/* Lighter hover state - subtle background tint only */ +.movieCard:hover { + background: rgba(0, 104, 74, 0.02); } -.selectionCheckbox { - position: absolute; - top: 12px; - right: 12px; - z-index: 2; - display: flex; - align-items: center; - gap: 4px; - background: rgba(255, 255, 255, 0.9); - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; +/* Stronger selected state - more prominent */ +.movieCard.selected { + border: 3px solid var(--mongodb-forest); + box-shadow: 0 4px 16px rgba(0, 104, 74, 0.2); + background: var(--mongodb-mint); + /* Reduce padding by 1px to compensate for thicker border */ + padding: 15px; } -.checkbox { - width: 16px; - height: 16px; - cursor: pointer; +/* Selected state overrides hover */ +.movieCard.selected:hover { + background: var(--mongodb-mint); } .moviePoster { @@ -58,13 +56,13 @@ width: 100%; height: 300px; margin-bottom: 16px; - background: #f5f5f5; - border-radius: 4px; + background: var(--mongodb-mint); + border-radius: var(--radius-md); overflow: hidden; } .moviePoster img { - border-radius: 4px; + border-radius: var(--radius-md); object-fit: cover; } @@ -74,91 +72,109 @@ justify-content: center; width: 100%; height: 100%; - color: #666; + color: var(--color-text-muted); font-size: 14px; text-align: center; - background: #f5f5f5; - border-radius: 4px; + background: var(--mongodb-mint); + border-radius: var(--radius-md); + font-weight: 500; } .movieInfo { margin-bottom: 16px; + /* Allow this section to grow and push button to bottom */ + flex: 1; } .movieTitle { margin: 0 0 8px 0; font-size: 18px; - font-weight: 600; + font-weight: 700; line-height: 1.3; - color: #333; + color: var(--mongodb-slate); } .movieYear { margin: 0 0 4px 0; - color: #666; + color: var(--color-text-secondary); font-size: 14px; + font-weight: 500; } .movieRating { margin: 0 0 4px 0; - color: #666; + color: var(--color-text-secondary); font-size: 14px; + font-weight: 500; } .vectorScore { - margin: 0 0 4px 0; - color: #0066cc; + margin: 0 0 8px 0; + color: var(--mongodb-forest); font-size: 13px; - font-weight: 500; - background: #e6f0ff; - padding: 4px 8px; - border-radius: 4px; + font-weight: 600; + background: var(--mongodb-mint); + padding: 6px 10px; + border-radius: var(--radius-md); display: inline-block; + border: 1px solid var(--mongodb-forest); } .movieGenres { margin: 0; - color: #888; + color: var(--color-text-muted); font-size: 12px; - font-style: italic; + font-weight: 500; } .detailsButton { display: block; width: 100%; - background: #0066cc; - color: white; + background: var(--mongodb-forest); + color: var(--mongodb-white); border: none; padding: 12px 16px; - border-radius: 4px; + border-radius: var(--radius-md); font-size: 14px; - font-weight: 500; + font-weight: 600; cursor: pointer; text-decoration: none; text-align: center; - transition: background-color 0.2s ease; + transition: all var(--transition-base); + box-shadow: var(--shadow-sm); + /* Keep button at bottom of card */ + margin-top: auto; } .detailsButton:hover { - background: #0052a3; + background: var(--mongodb-evergreen); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.detailsButton:active { + transform: translateY(0); } .noMovies { text-align: center; padding: 40px; - color: #666; + color: var(--color-text-secondary); + font-size: 1.1rem; } .pageTitle { margin: 0 0 16px 0; font-size: 32px; - color: #333; + color: var(--mongodb-slate); + font-weight: 700; } .movieCount { margin: 0 0 32px 0; - color: #666; + color: var(--color-text-secondary); font-size: 16px; + font-weight: 500; } /* Responsive Design */ diff --git a/mflix/client/app/components/MovieCard/MovieCard.tsx b/mflix/client/app/components/MovieCard/MovieCard.tsx index 7fa702c..fe487e3 100644 --- a/mflix/client/app/components/MovieCard/MovieCard.tsx +++ b/mflix/client/app/components/MovieCard/MovieCard.tsx @@ -14,6 +14,15 @@ import React from "react"; * such as image error handling and selection checkbox. */ +/** + * Validates that a poster URL is valid for Next.js Image component. + * Must be an absolute URL (http/https) or a relative path starting with / + */ +const isValidPosterUrl = (url: string | undefined): boolean => { + if (!url || typeof url !== 'string') return false; + return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'); +}; + interface MovieCardProps { movie: Movie; isSelected?: boolean; @@ -27,30 +36,28 @@ export default function MovieCard({ movie, isSelected = false, onSelectionChange console.warn(`Failed to load poster for: ${movie.title}`); }; - const handleCheckboxChange = (e: React.ChangeEvent) => { - if (onSelectionChange) { - onSelectionChange(movie._id, e.target.checked); + // Handle card click for selection (when checkbox is shown) + const handleCardClick = (e: React.MouseEvent) => { + // Don't toggle selection if clicking on the "Get Details" link + const target = e.target as HTMLElement; + if (target.closest('a')) { + return; + } + + if (showCheckbox && onSelectionChange) { + onSelectionChange(movie._id, !isSelected); } }; return ( -
- {showCheckbox && ( -
- -
- )} - +
- {movie.poster ? ( + {isValidPosterUrl(movie.poster) ? ( {`${movie.title} { + const result = await fetchMoviesByYear(); + if (!result.success || !result.data || result.data.length === 0) { + return { success: false, error: result.error || 'No year data available' }; + } + const years = result.data.map(stat => stat.year); + return { + success: true, + minYear: Math.min(...years), + maxYear: Math.max(...years) + }; +} + /** * Fetch directors with most movies and their statistics */ diff --git a/mflix/client/app/movie/[id]/not-found.module.css b/mflix/client/app/movie/[id]/not-found.module.css index b295429..15b7e54 100644 --- a/mflix/client/app/movie/[id]/not-found.module.css +++ b/mflix/client/app/movie/[id]/not-found.module.css @@ -31,7 +31,7 @@ } .backLink { - color: #0070f3; + color: var(--mongodb-forest); text-decoration: none; font-size: 1.1rem; font-weight: 500; @@ -39,12 +39,12 @@ } .backLink:hover { - color: #0051cc; + color: var(--mongodb-evergreen); text-decoration: underline; } .backLink:focus { - outline: 2px solid #0070f3; + outline: 2px solid var(--mongodb-forest); outline-offset: 2px; border-radius: 4px; } \ No newline at end of file diff --git a/mflix/client/app/movie/[id]/page.module.css b/mflix/client/app/movie/[id]/page.module.css index ba5866b..6433b74 100644 --- a/mflix/client/app/movie/[id]/page.module.css +++ b/mflix/client/app/movie/[id]/page.module.css @@ -22,14 +22,14 @@ } .backLink a { - color: #0070f3; + color: var(--mongodb-forest); text-decoration: none; font-weight: 500; transition: color 0.2s ease; } .backLink a:hover { - color: #0051cc; + color: var(--mongodb-evergreen); text-decoration: underline; } diff --git a/mflix/client/app/movie/[id]/page.tsx b/mflix/client/app/movie/[id]/page.tsx index 86395c4..ef6a612 100644 --- a/mflix/client/app/movie/[id]/page.tsx +++ b/mflix/client/app/movie/[id]/page.tsx @@ -11,6 +11,15 @@ import { Movie } from '@/types/movie'; import { ROUTES } from '@/lib/constants'; import pageStyles from './page.module.css'; +/** + * Validates that a poster URL is valid for Next.js Image component. + * Must be an absolute URL (http/https) or a relative path starting with / + */ +const isValidPosterUrl = (url: string | undefined): boolean => { + if (!url || typeof url !== 'string') return false; + return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/'); +}; + interface MovieDetailsPageProps { params: Promise<{ id: string; @@ -202,7 +211,7 @@ export default function MovieDetailsPage({ params }: MovieDetailsPageProps) { ) : (
- {movie.poster ? ( + {isValidPosterUrl(movie.poster) ? (
)} - {/* Batch Selection Controls */} - {!showAddForm && !showBatchEditForm && !showSearchModal && displayMovies.length > 0 && ( -
- {selectedMovies.size > 0 && ( - <> - - - - - )} -
- )} - {!isSearchMode && (
)} + + {/* Contextual Bottom Selection Bar */} + {selectedMovies.size > 0 && !showAddForm && !showBatchEditForm && !showSearchModal && ( +
+
+
+ + {selectedMovies.size} movie{selectedMovies.size !== 1 ? 's' : ''} selected + + +
+
+ + +
+
+
+ )}
); diff --git a/mflix/client/package.json b/mflix/client/package.json index 2151fee..552c14c 100644 --- a/mflix/client/package.json +++ b/mflix/client/package.json @@ -12,17 +12,17 @@ "lint": "eslint" }, "dependencies": { - "react": "19.2.0", - "react-dom": "19.2.0", - "next": "16.1.5" + "next": "^16.1.6", + "react": "^19.2.4", + "react-dom": "^19.2.4" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", - "eslint-config-next": "16.1.5", - "@eslint/eslintrc": "^3" + "eslint-config-next": "^16.1.6", + "typescript": "^5" } } diff --git a/mflix/server/java-spring/pom.xml b/mflix/server/java-spring/pom.xml index baff5c0..a7b3ae8 100644 --- a/mflix/server/java-spring/pom.xml +++ b/mflix/server/java-spring/pom.xml @@ -21,11 +21,11 @@ 21 2.8.13 - 4.0.0 - 3.19.0 - 1.12.0 + 5.1.0 + 3.20.0 + 1.13.0 1.17.8 - 1.0.0-beta3 + 1.11.0-beta19 @@ -50,7 +50,7 @@ me.paulschwarz - spring-dotenv + springboot3-dotenv ${dotenv.version} diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java index d696c3e..519f13b 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java @@ -85,7 +85,7 @@ public ResponseEntity>> getAllMovies( @RequestParam(defaultValue = "title") String sortBy, @Parameter(description = "Sort order: 'asc' or 'desc' (default: asc)") @RequestParam(defaultValue = "asc") String sortOrder) { - + MovieSearchQuery query = MovieSearchQuery.builder() .q(q) .genre(genre) @@ -97,16 +97,18 @@ public ResponseEntity>> getAllMovies( .sortBy(sortBy) .sortOrder(sortOrder) .build(); - + List movies = movieService.getAllMovies(query); - + + String message = "Found " + movies.size() + " movies"; + SuccessResponse> response = SuccessResponse.>builder() .success(true) - .message("Found " + movies.size() + " movies") + .message(message) .data(movies) .timestamp(Instant.now().toString()) .build(); - + return ResponseEntity.ok(response); } diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java index d293b23..3ccb9d5 100644 --- a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java +++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java @@ -376,7 +376,7 @@ public List getMoviesWithMostRecentComments(Integer lim int resultLimit = Math.clamp(limit != null ? limit : 10, 1, 50); // Build match criteria - Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030); + Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16); // Add movie ID filter if provided if (movieId != null && !movieId.trim().isEmpty()) { @@ -461,7 +461,7 @@ public List getMoviesByYearWithStats() { Aggregation aggregation = Aggregation.newAggregation( // STAGE 1: Match movies with valid year data Aggregation.match( - Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030) + Criteria.where(Movie.Fields.YEAR).type(16) ), // STAGE 2: Group by year and calculate statistics @@ -512,7 +512,7 @@ public List getDirectorsWithMostMovies(Integer limit) // STAGE 1: Match movies with directors and valid year Aggregation.match( Criteria.where(Movie.Fields.DIRECTORS).exists(true).ne(null).ne(List.of()) - .and(Movie.Fields.YEAR).type(16).gte(1800).lte(2030) + .and(Movie.Fields.YEAR).type(16) ), // STAGE 2: Unwind directors array diff --git a/mflix/server/js-express/package.json b/mflix/server/js-express/package.json index 85f09a2..ae1e7d3 100644 --- a/mflix/server/js-express/package.json +++ b/mflix/server/js-express/package.json @@ -19,26 +19,26 @@ "test:silent": "jest --silent" }, "dependencies": { - "cors": "^2.8.5", - "dotenv": "^17.2.3", - "express": "^5.1.0", - "mongodb": "^7.0.0", + "cors": "^2.8.6", + "dotenv": "^17.2.4", + "express": "^5.2.1", + "mongodb": "^7.1.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.1", "winston": "^3.19.0" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.14", - "@types/node": "^20.10.5", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/jest": "^30.0.0", + "@types/node": "^25.2.2", "@types/supertest": "^6.0.3", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", - "jest": "^29.7.0", - "supertest": "^7.1.4", - "ts-jest": "^29.1.1", + "jest": "^30.2.0", + "supertest": "^7.2.2", + "ts-jest": "^29.4.6", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.9.3" } } diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts index 78437ea..a4e516b 100644 --- a/mflix/server/js-express/src/controllers/movieController.ts +++ b/mflix/server/js-express/src/controllers/movieController.ts @@ -172,8 +172,8 @@ export async function getDistinctGenres( export async function getMovieById(req: Request, res: Response): Promise { const { id } = req.params; - // Validate ObjectId format - if (!ObjectId.isValid(id)) { + // Validate id is a string and ObjectId format + if (typeof id !== "string" || !ObjectId.isValid(id)) { res .status(400) .json( @@ -305,8 +305,8 @@ export async function updateMovie(req: Request, res: Response): Promise { const { id } = req.params; const updateData: UpdateMovieRequest = req.body; - // Validate ObjectId format - if (!ObjectId.isValid(id)) { + // Validate id is a string and ObjectId format + if (typeof id !== "string" || !ObjectId.isValid(id)) { res .status(400) .json( @@ -426,8 +426,8 @@ export async function updateMoviesBatch( export async function deleteMovie(req: Request, res: Response): Promise { const { id } = req.params; - // Validate ObjectId format - if (!ObjectId.isValid(id)) { + // Validate id is a string and ObjectId format + if (typeof id !== "string" || !ObjectId.isValid(id)) { res .status(400) .json( @@ -521,8 +521,8 @@ export async function findAndDeleteMovie( ): Promise { const { id } = req.params; - // Validate ObjectId format - if (!ObjectId.isValid(id)) { + // Validate id is a string and ObjectId format + if (typeof id !== "string" || !ObjectId.isValid(id)) { res .status(400) .json( @@ -967,7 +967,7 @@ export async function getMoviesWithMostRecentComments( // the collection { $match: { - year: { $type: "number", $gte: 1800, $lte: 2030 }, + year: { $type: "number" }, }, }, ]; @@ -1105,7 +1105,7 @@ export async function getMoviesByYearWithStats( // STAGE 1: Data quality filter { $match: { - year: { $type: "number", $gte: 1800, $lte: 2030 }, + year: { $type: "number" }, }, }, // STAGE 2: Group by year and calculate statistics @@ -1209,7 +1209,7 @@ export async function getDirectorsWithMostMovies( { $match: { directors: { $exists: true, $ne: null, $not: { $eq: [] } }, - year: { $type: "number", $gte: 1800, $lte: 2030 }, + year: { $type: "number" }, }, }, // STAGE 2: Unwind directors array diff --git a/mflix/server/js-express/tests/controllers/movieController.test.ts b/mflix/server/js-express/tests/controllers/movieController.test.ts index f6d5b2e..433994b 100644 --- a/mflix/server/js-express/tests/controllers/movieController.test.ts +++ b/mflix/server/js-express/tests/controllers/movieController.test.ts @@ -183,7 +183,7 @@ describe("Movie Controller Tests", () => { const testMovies = [{ _id: TEST_MOVIE_ID, title: "Action Movie" }]; mockRequest.query = { genre: "Action", - year: "2024", + year: "2010", minRating: "7.0", limit: "10", sortBy: "year", @@ -195,7 +195,7 @@ describe("Movie Controller Tests", () => { expect(mockFind).toHaveBeenCalledWith({ genres: { $regex: new RegExp("Action", "i") }, - year: 2024, + year: 2010, "imdb.rating": { $gte: 7.0 }, }); expect(mockCreateSuccessResponse).toHaveBeenCalledWith( @@ -203,6 +203,7 @@ describe("Movie Controller Tests", () => { "Found 1 movies" ); }); + }); describe("getMovieById", () => { diff --git a/mflix/server/python-fastapi/requirements.in b/mflix/server/python-fastapi/requirements.in index a54f67e..c50a667 100644 --- a/mflix/server/python-fastapi/requirements.in +++ b/mflix/server/python-fastapi/requirements.in @@ -2,21 +2,21 @@ # 1. CORE WEB FRAMEWORK & ASGI SERVER # FastAPI and its main components. # ------------------------------------------------------------------------------ -fastapi~=0.120.1 # The main web framework -starlette~=0.49.1 # FastAPI's underlying ASGI toolkit +fastapi~=0.120.4 # The main web framework +starlette~=0.49.3 # FastAPI's underlying ASGI toolkit uvicorn~=0.38.0 # Production-ready ASGI server -uvloop~=0.22.0 # Optional: High-performance event loop for uvicorn -websockets~=15.0.0 # For WebSocket support -watchfiles~=1.1.0 # For hot-reloading in development +uvloop~=0.22.1 # Optional: High-performance event loop for uvicorn +websockets~=15.0.1 # For WebSocket support +watchfiles~=1.1.1 # For hot-reloading in development # ============================================================================== # 2. DATA VALIDATION & CORE UTILITIES # Primary libraries for data models and environment config. # ------------------------------------------------------------------------------ -pydantic~=2.12.0 # Data validation and settings management -python-dotenv~=1.1.0 # For loading configuration from .env files +pydantic~=2.12.5 # Data validation and settings management +python-dotenv~=1.1.1 # For loading configuration from .env files python-multipart>=0.0.22 # For parsing form data and file uploads -PyYAML~=6.0.0 # For handling YAML configuration or data +PyYAML~=6.0.3 # For handling YAML configuration or data # ============================================================================== # 3. DATABASE & CONNECTIVITY @@ -29,33 +29,33 @@ dnspython~=2.8.0 # Required for SRV record lookups by pymongo (e.g., Mong # 4. HTTP CLIENT & UTILITIES # Primary libraries for making external HTTP requests. # ------------------------------------------------------------------------------ -httpx~=0.28.0 # Asynchronous HTTP client for requests to external APIs +httpx~=0.28.1 # Asynchronous HTTP client for requests to external APIs email-validator~=2.3.0 # Utility for validating email addresses -voyageai~=0.3.5 # Vector embeddings API client +voyageai~=0.3.7 # Vector embeddings API client urllib3>=2.6.3 # HTTP library # ============================================================================== # 5. CLI & DEVELOPMENT TOOLS # Tools for building command-line interfaces for management tasks. # ------------------------------------------------------------------------------ -typer~=0.20.0 # Library for creating command-line applications -fastapi-cli~=0.0.0 # Tools to run and manage FastAPI projects -fastapi-cloud-cli~=0.3.0 # Tools for cloud deployment (specific to your pipeline) +typer~=0.20.1 # Library for creating command-line applications +fastapi-cli~=0.0.20 # Tools to run and manage FastAPI projects +fastapi-cloud-cli~=0.3.1 # Tools for cloud deployment (specific to your pipeline) # ============================================================================== # 6. TESTING & MONITORING # Frameworks for ensuring code quality and production health. # ------------------------------------------------------------------------------ -pytest~=8.4.0 # Primary testing framework +pytest~=8.4.2 # Primary testing framework pytest-asyncio~=1.2.0 # Plugin to make asynchronous tests easy with pytest -sentry-sdk~=2.42.0 # For error tracking and performance monitoring +sentry-sdk~=2.42.1 # For error tracking and performance monitoring # ============================================================================== # 7. LOGGING AND TERMINAL OUTPUT # Libraries for rich console output and debugging. # ------------------------------------------------------------------------------ rich~=14.2.0 # For rich, formatted terminal output -rich-toolkit~=0.15.0 # Extensions for the 'rich' library +rich-toolkit~=0.15.1 # Extensions for the 'rich' library # ============================================================================== # 8. TRANSITIVE DEPENDENCY CONSTRAINTS @@ -63,4 +63,4 @@ rich-toolkit~=0.15.0 # Extensions for the 'rich' library # ------------------------------------------------------------------------------ filelock>=3.20.3 # Transitive dep via huggingface-hub aiohttp>=3.13.3 # Transitive dep via voyageai -orjson>=3.11.5 # Transitive dep via langsmith (CVE fix) +orjson>=3.11.7 # Transitive dep via langsmith (CVE fix) diff --git a/mflix/server/python-fastapi/requirements.txt b/mflix/server/python-fastapi/requirements.txt index 3113cb3..29e4311 100644 --- a/mflix/server/python-fastapi/requirements.txt +++ b/mflix/server/python-fastapi/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.13 # by the following command: # -# pip-compile --output-file=requirements.txt requirements.in +# pip-compile requirements.in # aiohappyeyeballs==2.6.1 # via aiohttp @@ -14,18 +14,18 @@ aiolimiter==1.2.1 # via voyageai aiosignal==1.4.0 # via aiohttp -annotated-doc==0.0.3 +annotated-doc==0.0.4 # via fastapi annotated-types==0.7.0 # via pydantic -anyio==4.11.0 +anyio==4.12.1 # via # httpx # starlette # watchfiles attrs==25.4.0 # via aiohttp -certifi==2025.10.5 +certifi==2026.1.4 # via # httpcore # httpx @@ -33,7 +33,7 @@ certifi==2025.10.5 # sentry-sdk charset-normalizer==3.4.4 # via requests -click==8.3.0 +click==8.3.1 # via # rich-toolkit # typer @@ -48,12 +48,14 @@ email-validator==2.3.0 # via # -r requirements.in # pydantic -fastapi==0.120.2 +fastapi==0.120.4 # via -r requirements.in -fastapi-cli==0.0.14 +fastapi-cli==0.0.20 # via -r requirements.in fastapi-cloud-cli==0.3.1 # via -r requirements.in +ffmpeg-python==0.2.0 + # via voyageai filelock==3.20.3 # via # -r requirements.in @@ -62,8 +64,10 @@ frozenlist==1.8.0 # via # aiohttp # aiosignal -fsspec==2025.10.0 +fsspec==2026.2.0 # via huggingface-hub +future==1.0.0 + # via ffmpeg-python h11==0.16.0 # via # httpcore @@ -80,7 +84,7 @@ httpx==0.28.1 # fastapi-cloud-cli # huggingface-hub # langsmith -huggingface-hub==1.0.1 +huggingface-hub==1.4.1 # via tokenizers idna==3.11 # via @@ -95,33 +99,33 @@ jsonpatch==1.33 # via langchain-core jsonpointer==3.0.0 # via jsonpatch -langchain-core==1.2.5 +langchain-core==1.2.9 # via langchain-text-splitters -langchain-text-splitters==1.0.0 +langchain-text-splitters==1.1.0 # via voyageai -langsmith==0.4.40 +langsmith==0.6.9 # via langchain-core markdown-it-py==4.0.0 # via rich mdurl==0.1.2 # via markdown-it-py -multidict==6.7.0 +multidict==6.7.1 # via # aiohttp # yarl -numpy==2.3.4 +numpy==2.4.2 # via voyageai -orjson==3.11.5 +orjson==3.11.7 # via # -r requirements.in # langsmith -packaging==25.0 +packaging==26.0 # via # huggingface-hub # langchain-core # langsmith # pytest -pillow==12.0.0 +pillow==12.1.0 # via voyageai pluggy==1.6.0 # via pytest @@ -129,7 +133,7 @@ propcache==0.4.1 # via # aiohttp # yarl -pydantic[email]==2.12.3 +pydantic[email]==2.12.5 # via # -r requirements.in # fastapi @@ -137,7 +141,7 @@ pydantic[email]==2.12.3 # langchain-core # langsmith # voyageai -pydantic-core==2.41.4 +pydantic-core==2.41.5 # via pydantic pygments==2.19.2 # via @@ -180,7 +184,7 @@ rich-toolkit==0.15.1 # -r requirements.in # fastapi-cli # fastapi-cloud-cli -rignore==0.7.1 +rignore==0.7.6 # via fastapi-cloud-cli sentry-sdk==2.42.1 # via @@ -190,26 +194,24 @@ shellingham==1.5.4 # via # huggingface-hub # typer -sniffio==1.3.1 - # via anyio -starlette==0.49.1 +starlette==0.49.3 # via # -r requirements.in # fastapi -tenacity==9.1.2 +tenacity==9.1.3 # via # langchain-core # voyageai -tokenizers==0.22.1 +tokenizers==0.22.2 # via voyageai -tqdm==4.67.1 +tqdm==4.67.3 # via huggingface-hub -typer==0.20.0 +typer==0.20.1 # via # -r requirements.in # fastapi-cli # fastapi-cloud-cli -typer-slim==0.20.0 +typer-slim==0.21.1 # via huggingface-hub typing-extensions==4.15.0 # via @@ -229,8 +231,10 @@ urllib3==2.6.3 # -r requirements.in # requests # sentry-sdk -uuid-utils==0.12.0 - # via langchain-core +uuid-utils==0.14.0 + # via + # langchain-core + # langsmith uvicorn[standard]==0.38.0 # via # -r requirements.in @@ -240,7 +244,7 @@ uvloop==0.22.1 # via # -r requirements.in # uvicorn -voyageai==0.3.5 +voyageai==0.3.7 # via -r requirements.in watchfiles==1.1.1 # via @@ -250,6 +254,8 @@ websockets==15.0.1 # via # -r requirements.in # uvicorn +xxhash==3.6.0 + # via langsmith yarl==1.22.0 # via aiohttp zstandard==0.25.0 diff --git a/mflix/server/python-fastapi/src/routers/movies.py b/mflix/server/python-fastapi/src/routers/movies.py index bb30c32..1da9b6a 100644 --- a/mflix/server/python-fastapi/src/routers/movies.py +++ b/mflix/server/python-fastapi/src/routers/movies.py @@ -554,7 +554,7 @@ async def get_all_movies( filter_dict["title"] = {"$regex": title, "$options": "i"} if genre: filter_dict["genres"] = {"$regex": genre, "$options": "i"} - if year: + if isinstance(year, int): filter_dict["year"] = year if min_rating is not None or max_rating is not None: rating_filter = {} @@ -593,8 +593,10 @@ async def get_all_movies( movie["year"] = None movies.append(movie) + # Return the results wrapped in a SuccessResponse - return create_success_response(movies, f"Found {len(movies)} movies.") + message = f"Found {len(movies)} movies." + return create_success_response(movies, message) """ POST /api/movies/ @@ -1014,7 +1016,7 @@ async def aggregate_movies_recent_commented( # Filter movies to only those with valid year data { "$match": { - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + "year": {"$type": "number"} } } ] @@ -1160,7 +1162,7 @@ async def aggregate_movies_by_year(): # Tip: Filter early to reduce dataset size and improve performance { "$match": { - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} + "year": {"$type": "number"} } }, @@ -1289,7 +1291,7 @@ async def aggregate_directors_most_movies( { "$match": { "directors": {"$exists": True, "$ne": None, "$ne": []}, # Has directors array - "year": {"$type": "number", "$gte": 1800, "$lte": 2030} # Valid year range + "year": {"$type": "number"} # Valid year (numeric) } }, diff --git a/mflix/server/python-fastapi/tests/test_movie_routes.py b/mflix/server/python-fastapi/tests/test_movie_routes.py index 5d74779..8bb335d 100644 --- a/mflix/server/python-fastapi/tests/test_movie_routes.py +++ b/mflix/server/python-fastapi/tests/test_movie_routes.py @@ -394,7 +394,7 @@ async def test_get_all_movies_database_error(self, mock_get_collection): # Call the route handler from src.routers.movies import get_all_movies - with pytest.raises(HTTPException) as e: + with pytest.raises(HTTPException) as e: await get_all_movies() # Assertions