diff --git a/.github/instructions/sonarqube_mcp.instructions.md b/.github/instructions/sonarqube_mcp.instructions.md new file mode 100644 index 0000000..5cbb5e6 --- /dev/null +++ b/.github/instructions/sonarqube_mcp.instructions.md @@ -0,0 +1,42 @@ +--- +applyTo: "**/*" +--- + +These are some guidelines when using the SonarQube MCP server. + +# Important Tool Guidelines + +## Basic usage +- **IMPORTANT**: After you finish generating or modifying any code files at the very end of the task, you MUST call the `analyze_file_list` tool (if it exists) to analyze the files you created or modified. +- **IMPORTANT**: When starting a new task, you MUST disable automatic analysis with the `toggle_automatic_analysis` tool if it exists. +- **IMPORTANT**: When you are done generating code at the very end of the task, you MUST re-enable automatic analysis with the `toggle_automatic_analysis` tool if it exists. + +## Project Keys +- When a user mentions a project key, use `search_my_sonarqube_projects` first to find the exact project key +- Don't guess project keys - always look them up + +## Code Language Detection +- When analyzing code snippets, try to detect the programming language from the code syntax +- If unclear, ask the user or make an educated guess based on syntax + +## Branch and Pull Request Context +- Many operations support branch-specific analysis +- If user mentions working on a feature branch, include the branch parameter + +## Code Issues and Violations +- After fixing issues, do not attempt to verify them using `search_sonar_issues_in_projects`, as the server will not yet reflect the updates + +# Common Troubleshooting + +## Authentication Issues +- SonarQube requires USER tokens (not project tokens) +- When the error `SonarQube answered with Not authorized` occurs, verify the token type + +## Project Not Found +- Use `search_my_sonarqube_projects` to find available projects +- Verify project key spelling and format + +## Code Analysis Issues +- Ensure programming language is correctly specified +- Remind users that snippet analysis doesn't replace full project scans +- Provide full file content for better analysis results diff --git a/.specify/feature.json b/.specify/feature.json index 2330ca5..d24a115 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/025-normalize-movie-genres" + "feature_directory": "specs/017-home-movie-cards" } diff --git a/.vscode/settings.json b/.vscode/settings.json index f363fdc..52b0a33 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "files.exclude": { "**/.vs":true, "**/.codex": true, - "**/.ai": true, + "**/.ai": false, "**/node_modules": true, "**/bin": true, "**/obj": true, @@ -17,6 +17,10 @@ "search.exclude": { "**/dist": true, "**/*.js.map": true + }, + "sonarlint.connectedMode.project": { + "connectionId": "marciomyst", + "projectKey": "marciomyst_SmartMovieCatalog" } } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 313c849..b3a414a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,7 +127,7 @@ Do not read, modify, or base analysis on generated/vendor output unless explicit - `test:` -Current Spec Kit plan: `specs/013-view-movie-details/plan.md`. +Current Spec Kit plan: `specs/017-home-movie-cards/plan.md`. Before using Spec Kit skills, read `.specify/memory/constitution.md`. If the spec, plan, or implementation touches backend, API, contracts, domain, diff --git a/backend/src/SmartMovieCatalog.Api/Features/Movies/GetMovieById/GetMovieByIdEndpoint.cs b/backend/src/SmartMovieCatalog.Api/Features/Movies/GetMovieById/GetMovieByIdEndpoint.cs index dfad8af..1d77d5a 100644 --- a/backend/src/SmartMovieCatalog.Api/Features/Movies/GetMovieById/GetMovieByIdEndpoint.cs +++ b/backend/src/SmartMovieCatalog.Api/Features/Movies/GetMovieById/GetMovieByIdEndpoint.cs @@ -8,6 +8,7 @@ namespace SmartMovieCatalog.Api.Features.Movies.GetMovieById; public static class GetMovieByIdEndpoint { + private const string ProblemJsonContentType = "application/problem+json"; public static void MapGetMovieById(this IEndpointRouteBuilder app) { app.MapGet("/api/movies/{id}", HandleAsync) @@ -15,8 +16,8 @@ public static void MapGetMovieById(this IEndpointRouteBuilder app) .WithTags("Movies") .AllowAnonymous() .Produces(StatusCodes.Status200OK) - .Produces(StatusCodes.Status400BadRequest, "application/problem+json") - .Produces(StatusCodes.Status404NotFound, "application/problem+json"); + .Produces(StatusCodes.Status400BadRequest, ProblemJsonContentType) + .Produces(StatusCodes.Status404NotFound, ProblemJsonContentType); } private static async Task HandleAsync( @@ -30,7 +31,7 @@ private static async Task HandleAsync( return Results.Json( CreateInvalidMovieIdProblem(httpContext), statusCode: StatusCodes.Status400BadRequest, - contentType: "application/problem+json"); + contentType: ProblemJsonContentType); } Result result = @@ -57,7 +58,7 @@ await messageBus.InvokeAsync>( _ => Results.Json( CreateMovieNotFoundProblem(httpContext, id), statusCode: StatusCodes.Status404NotFound, - contentType: "application/problem+json")); + contentType: ProblemJsonContentType)); } private static ProblemDetails CreateInvalidMovieIdProblem(HttpContext httpContext) diff --git a/backend/tests/SmartMovieCatalog.Domain.Tests/Common/ValueObjectTests.cs b/backend/tests/SmartMovieCatalog.Domain.Tests/Common/ValueObjectTests.cs new file mode 100644 index 0000000..daa0619 --- /dev/null +++ b/backend/tests/SmartMovieCatalog.Domain.Tests/Common/ValueObjectTests.cs @@ -0,0 +1,70 @@ +using SmartMovieCatalog.Domain.Common; + +namespace SmartMovieCatalog.Domain.Tests.Common; + +public sealed class ValueObjectTests +{ + [Fact] + public void Equals_WithSameTypeAndSameComponents_ReturnsTrue() + { + SampleValueObject left = new("same", 7); + SampleValueObject right = new("same", 7); + + Assert.True(left.Equals(right)); + Assert.Equal(left.GetHashCode(), right.GetHashCode()); + } + + [Fact] + public void Equals_WithDifferentComponents_ReturnsFalse() + { + SampleValueObject left = new("left", 7); + SampleValueObject right = new("right", 8); + + Assert.False(left.Equals(right)); + } + + [Fact] + public void Equals_WithDifferentRuntimeType_ReturnsFalse() + { + SampleValueObject sample = new("same", 7); + OtherValueObject other = new("same", 7); + + Assert.False(sample.Equals(other)); + } + + private sealed class SampleValueObject : ValueObject + { + private readonly string _name; + private readonly int _version; + + public SampleValueObject(string name, int version) + { + _name = name; + _version = version; + } + + protected override IEnumerable GetEqualityComponents() + { + yield return _name; + yield return _version; + } + } + + private sealed class OtherValueObject : ValueObject + { + private readonly string _name; + private readonly int _version; + + public OtherValueObject(string name, int version) + { + _name = name; + _version = version; + } + + protected override IEnumerable GetEqualityComponents() + { + yield return _name; + yield return _version; + } + } +} diff --git a/backend/tests/SmartMovieCatalog.Domain.Tests/Movies/MovieValidationTests.cs b/backend/tests/SmartMovieCatalog.Domain.Tests/Movies/MovieValidationTests.cs index 0641218..f04d8ec 100644 --- a/backend/tests/SmartMovieCatalog.Domain.Tests/Movies/MovieValidationTests.cs +++ b/backend/tests/SmartMovieCatalog.Domain.Tests/Movies/MovieValidationTests.cs @@ -109,11 +109,28 @@ public void Create_WithoutExternalIdAndImage_KeepsValuesNull() [InlineData("https://image.tmdb.org/t/p/w500/example.jpg")] [InlineData("//image.tmdb.org/t/p/w500/example.jpg")] [InlineData("p/example-card.jpg")] + [InlineData("/p\\example-card.jpg")] public void Create_WithNonRelativeImage_Throws(string image) { Assert.Throws(() => CreateMovie(image: image)); } + [Fact] + public void Create_WithRelativeImagePath_KeepsNormalizedPath() + { + Movie movie = CreateMovie(image: "/p/example-card.jpg"); + + Assert.Equal("/p/example-card.jpg", movie.Image); + } + + [Fact] + public void Create_WithLowercaseCountryCode_NormalizesToUppercase() + { + Movie movie = CreateMovie(countryCode: "br"); + + Assert.Equal("BR", movie.CountryCode); + } + [Fact] public void Genre_WithBlankName_Throws() { diff --git a/backend/tests/SmartMovieCatalog.Domain.Tests/Users/UserIdTests.cs b/backend/tests/SmartMovieCatalog.Domain.Tests/Users/UserIdTests.cs index dec19fb..f06cbaf 100644 --- a/backend/tests/SmartMovieCatalog.Domain.Tests/Users/UserIdTests.cs +++ b/backend/tests/SmartMovieCatalog.Domain.Tests/Users/UserIdTests.cs @@ -26,4 +26,22 @@ public void From_WithSameGuid_CreatesEqualIds() Assert.Equal(UserId.From(value), UserId.From(value)); } + + [Fact] + public void From_WithDifferentGuid_CreatesDifferentIds() + { + UserId first = UserId.From(Guid.NewGuid()); + UserId second = UserId.From(Guid.NewGuid()); + + Assert.NotEqual(first, second); + } + + [Fact] + public void Equals_WithNullOrDifferentType_ReturnsFalse() + { + UserId userId = UserId.New(); + + Assert.False(userId.Equals(null)); + Assert.False(userId.Equals("not-a-user-id")); + } } diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 56f293d..6965fd9 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -1,7 +1,7 @@ # Frontend ## Current State -The frontend is an Angular 21 SPA in `frontend`. The root route renders the authentication login page, and `/catalog` renders the public movie catalog browsing page. +The frontend is an Angular 21 SPA in `frontend`. The root route renders the public home page movie section, `/login` renders the authentication login page, and `/catalog` renders the public movie catalog browsing page. Primary references: @@ -11,7 +11,8 @@ Primary references: - `frontend/DESIGN.md` ## Routing -- `/` displays the authentication login page. +- `/` displays the public home page with movie cards. +- `/login` displays the authentication login page. - `/catalog` displays the public V1 catalog page. - `/movies/:id` displays movie details for a single movie. - `/catalog` reads `query`, `page`, and `pageSize` from URL query parameters. diff --git a/frontend/src/app/app.html b/frontend/src/app/app.html index 7041801..47c5f13 100644 --- a/frontend/src/app/app.html +++ b/frontend/src/app/app.html @@ -2,7 +2,9 @@
Smart Movie Catalog
diff --git a/frontend/src/app/app.spec.ts b/frontend/src/app/app.spec.ts index a04e860..7577e4a 100644 --- a/frontend/src/app/app.spec.ts +++ b/frontend/src/app/app.spec.ts @@ -4,6 +4,7 @@ import { of } from 'rxjs'; import { App } from './app'; import { LoginPage } from './auth/login-page/login-page'; import { CatalogPage } from './catalog/catalog-page/catalog-page'; +import { HomePage } from './home/home-page/home-page'; import { MovieDetails, PagedMovieSummaryResponse } from './movies/movie.models'; import { MovieDetailsPage } from './movies/movie-details-page/movie-details-page'; import { MoviesApi } from './movies/movies-api'; @@ -45,7 +46,8 @@ describe('App', () => { imports: [App], providers: [ provideRouter([ - { path: '', component: LoginPage }, + { path: '', component: HomePage }, + { path: 'login', component: LoginPage }, { path: 'catalog', component: CatalogPage }, { path: 'movies/:id', component: MovieDetailsPage } ]), @@ -70,12 +72,27 @@ describe('App', () => { expect(component).toBeTruthy(); }); - it('should render the app brand and catalog navigation link', () => { + it('should render the app brand and primary navigation links', () => { fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('.brand-link')?.textContent).toContain('Smart Movie Catalog'); + expect(compiled.querySelector('nav a[routerLink="/"]')?.textContent).toContain('Home'); expect(compiled.querySelector('a[routerLink="/catalog"]')?.textContent).toContain('Catalog'); + expect(compiled.querySelector('a[routerLink="/login"]')?.textContent).toContain('Login'); + }); + + it('should render the public home route', async () => { + fixture.detectChanges(); + + await router.navigateByUrl('/'); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('app-home-page')).toBeTruthy(); + expect(compiled.textContent).toContain('Latest from the catalog'); }); it('should render the public catalog route without an auth guard', async () => { @@ -103,4 +120,17 @@ describe('App', () => { expect(compiled.querySelector('app-movie-details-page')).toBeTruthy(); expect(compiled.textContent).toContain('Central do Brasil'); }); + + it('should render the login route', async () => { + fixture.detectChanges(); + + await router.navigateByUrl('/login'); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('app-login-page')).toBeTruthy(); + expect(compiled.textContent).toContain('Smart Movie Catalog'); + }); }); diff --git a/frontend/src/app/catalog/catalog-page/catalog-page.spec.ts b/frontend/src/app/catalog/catalog-page/catalog-page.spec.ts index 257e270..d255473 100644 --- a/frontend/src/app/catalog/catalog-page/catalog-page.spec.ts +++ b/frontend/src/app/catalog/catalog-page/catalog-page.spec.ts @@ -244,4 +244,61 @@ describe('CatalogPage', () => { const link = fixture.nativeElement.querySelector('.movie-card') as HTMLAnchorElement; expect(link.getAttribute('href')).toBe('/movies/movie-1'); }); + + it('should render singular result summary when only one movie is available', () => { + moviesApi.listMovies.mockReturnValue(of({ + ...movieResponse, + totalCount: 1 + })); + createComponent(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('1 movie'); + }); + + it('should not navigate to previous/next page when pagination is unavailable', () => { + moviesApi.listMovies.mockReturnValue(of({ + ...movieResponse, + hasPreviousPage: false, + hasNextPage: false + })); + createComponent(); + + const component = fixture.componentInstance as unknown as { + previousPage: () => void; + nextPage: () => void; + }; + + component.previousPage(); + component.nextPage(); + + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should keep absolute poster URLs unchanged', () => { + moviesApi.listMovies.mockReturnValue(of({ + ...movieResponse, + items: [{ + ...movieResponse.items[0], + posterUrl: 'https://image.tmdb.org/t/p/w342/external.jpg' + }] + })); + createComponent(); + + const image = fixture.nativeElement.querySelector('.poster-frame img') as HTMLImageElement; + expect(image.src).toBe('https://image.tmdb.org/t/p/w342/external.jpg'); + }); + + it('should avoid duplicate API calls when query state does not change', () => { + createComponent(); + moviesApi.listMovies.mockClear(); + + queryParamMap.next(convertToParamMap({ + query: '', + page: '1', + pageSize: '12' + })); + + expect(moviesApi.listMovies).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/src/app/home/home-page/home-page.css b/frontend/src/app/home/home-page/home-page.css new file mode 100644 index 0000000..d3fffcd --- /dev/null +++ b/frontend/src/app/home/home-page/home-page.css @@ -0,0 +1,176 @@ +:host { + display: block; +} + +.home-page { + width: min(100%, 80rem); + margin: 0 auto; + padding: 6rem 1.5rem 3rem; + color: #f4f4f5; +} + +.home-header { + margin-bottom: 1.75rem; +} + +.home-kicker { + margin: 0 0 0.45rem; + color: #f59e0b; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.home-header h1 { + margin: 0; + font-family: 'Plus Jakarta Sans', Inter, Arial, sans-serif; + font-size: clamp(2.25rem, 6vw, 3.8rem); + font-weight: 800; + line-height: 1; + letter-spacing: 0; +} + +.home-subtitle { + margin: 0.9rem 0 0; + color: #d4d4d8; + font-size: 1rem; +} + +.home-state { + display: grid; + place-items: center; + min-height: 18rem; + gap: 0.75rem; + border: 1px solid #303038; + border-radius: 0.75rem; + background: #16161b; + text-align: center; + color: #d4d4d8; +} + +.home-state .material-symbols-outlined { + display: grid; + place-items: center; + width: 3rem; + height: 3rem; + border-radius: 999px; + background: rgb(245 158 11 / 0.16); + color: #f59e0b; +} + +.home-state h2 { + margin: 0; + font-family: 'Plus Jakarta Sans', Inter, Arial, sans-serif; + font-size: 1.4rem; + letter-spacing: 0; + color: #fafafa; +} + +.home-state p { + margin: 0; +} + +.home-state-error .material-symbols-outlined { + background: rgb(220 38 38 / 0.18); + color: #fca5a5; +} + +.movie-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); + gap: 1rem; +} + +.movie-card { + position: relative; + min-height: 21rem; + overflow: hidden; + border: 1px solid #2f2f36; + border-radius: 0.75rem; + background: #191920; + color: #fafafa; + text-decoration: none; + transition: border-color 160ms ease, transform 160ms ease; +} + +.movie-card:hover, +.movie-card:focus-visible { + border-color: rgb(14 165 233 / 0.85); + transform: translateY(-0.15rem); + outline: 0; +} + +.poster-frame { + position: absolute; + inset: 0; +} + +.poster-frame::after { + position: absolute; + inset: 0; + content: ''; + background: linear-gradient(to top, rgb(0 0 0 / 0.93), rgb(0 0 0 / 0.44), transparent); +} + +.poster-frame img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.poster-placeholder { + display: grid; + place-items: center; + width: 100%; + height: 100%; + background: linear-gradient(145deg, #2a2a33, #0f1117); + color: #9ca3af; +} + +.poster-placeholder .material-symbols-outlined { + font-size: 3rem; +} + +.movie-card-content { + position: absolute; + inset-inline: 0; + bottom: 0; + z-index: 1; + padding: 1rem; +} + +.movie-card-content h2 { + margin: 0 0 0.5rem; + font-family: 'Plus Jakarta Sans', Inter, Arial, sans-serif; + font-size: 1.08rem; + line-height: 1.2; + letter-spacing: 0; +} + +.movie-meta, +.movie-genres, +.movie-director { + margin: 0 0 0.35rem; + font-size: 0.84rem; + line-height: 1.35; +} + +.movie-meta { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + color: #facc15; + font-weight: 700; +} + +.movie-director, +.movie-genres { + color: #d4d4d8; +} + +@media (max-width: 48rem) { + .home-page { + padding-inline: 1rem; + } +} diff --git a/frontend/src/app/home/home-page/home-page.html b/frontend/src/app/home/home-page/home-page.html new file mode 100644 index 0000000..4579c28 --- /dev/null +++ b/frontend/src/app/home/home-page/home-page.html @@ -0,0 +1,53 @@ +
+
+

Smart Movie Catalog

+

Latest from the catalog

+

A curated preview of six titles available now.

+
+ +
+ +

Loading movies

+
+ +
+ +

No movies yet

+

The catalog is ready for the first title.

+
+ + + + + + +
diff --git a/frontend/src/app/home/home-page/home-page.spec.ts b/frontend/src/app/home/home-page/home-page.spec.ts new file mode 100644 index 0000000..f2ec165 --- /dev/null +++ b/frontend/src/app/home/home-page/home-page.spec.ts @@ -0,0 +1,146 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { NEVER, of, throwError } from 'rxjs'; +import { MovieListQuery, PagedMovieSummaryResponse } from '../../movies/movie.models'; +import { MoviesApi } from '../../movies/movies-api'; +import { HomePage } from './home-page'; + +describe('HomePage', () => { + let fixture: ComponentFixture; + let moviesApi: { + listMovies: ReturnType ReturnType>>; + }; + + function createResponse(count: number): PagedMovieSummaryResponse { + return { + items: Array.from({ length: count }, (_item, index) => ({ + id: `movie-${index + 1}`, + title: `Movie ${index + 1}`, + releaseYear: 2000 + index, + countryCode: 'BR', + genres: index % 2 === 0 ? ['Drama'] : [], + director: index % 2 === 0 ? `Director ${index + 1}` : null, + posterUrl: index % 3 === 0 ? `/p/poster-${index + 1}.jpg` : null, + createdAt: '2026-05-09T00:00:00Z' + })), + page: 1, + pageSize: 6, + totalCount: count, + totalPages: count > 0 ? 1 : 0, + hasPreviousPage: false, + hasNextPage: false + }; + } + + beforeEach(async () => { + moviesApi = { + listMovies: vi.fn(() => of(createResponse(6))) + }; + + await TestBed.configureTestingModule({ + imports: [HomePage], + providers: [ + provideRouter([]), + { provide: MoviesApi, useValue: moviesApi } + ] + }).compileComponents(); + }); + + function createComponent(): void { + fixture = TestBed.createComponent(HomePage); + fixture.detectChanges(); + } + + it('should request movies with page=1 and pageSize=6', () => { + createComponent(); + + expect(moviesApi.listMovies).toHaveBeenCalledWith({ page: 1, pageSize: 6 }); + }); + + it('should render loading state while request is pending', () => { + moviesApi.listMovies.mockReturnValue(NEVER); + createComponent(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('Loading movies'); + }); + + it('should render exactly six cards when six or more movies are available', () => { + moviesApi.listMovies.mockReturnValue(of(createResponse(8))); + createComponent(); + + const cards = fixture.nativeElement.querySelectorAll('.movie-card'); + expect(cards).toHaveLength(6); + }); + + it('should render fewer cards when fewer movies are available', () => { + moviesApi.listMovies.mockReturnValue(of(createResponse(3))); + createComponent(); + + const cards = fixture.nativeElement.querySelectorAll('.movie-card'); + expect(cards).toHaveLength(3); + }); + + it('should render a poster placeholder when posterUrl is absent', () => { + moviesApi.listMovies.mockReturnValue(of(createResponse(2))); + createComponent(); + + const placeholder = fixture.nativeElement.querySelector('.poster-placeholder'); + expect(placeholder).toBeTruthy(); + }); + + it('should render empty state when no movies are available', () => { + moviesApi.listMovies.mockReturnValue(of(createResponse(0))); + createComponent(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('No movies yet'); + }); + + it('should render generic error state on request failure', () => { + moviesApi.listMovies.mockReturnValue(throwError(() => new Error('network'))); + createComponent(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('Catalog unavailable'); + expect(compiled.textContent).toContain('Try again in a moment.'); + }); + + it('should link each card to the corresponding details route', () => { + moviesApi.listMovies.mockReturnValue(of(createResponse(2))); + createComponent(); + + const link = fixture.nativeElement.querySelector('.movie-card') as HTMLAnchorElement; + expect(link.getAttribute('href')).toBe('/movies/movie-1'); + expect(link.getAttribute('aria-label')).toContain('Open details for Movie 1'); + }); + + it('should render cards as keyboard and pointer accessible links', () => { + moviesApi.listMovies.mockReturnValue(of(createResponse(2))); + createComponent(); + + const links = fixture.nativeElement.querySelectorAll('.movie-card'); + expect(links).toHaveLength(2); + + for (const link of links) { + expect(link.tagName).toBe('A'); + expect(link.getAttribute('href')).toContain('/movies/'); + expect(link.getAttribute('aria-label')).toContain('Open details for'); + } + }); + + it('should avoid unsupported product terminology in home copy', () => { + createComponent(); + + const compiled = fixture.nativeElement as HTMLElement; + const text = (compiled.textContent ?? '').toLowerCase(); + + expect(text).not.toContain('rag'); + expect(text).not.toContain('qdrant'); + expect(text).not.toContain('vector search'); + expect(text).not.toContain('semantic search'); + expect(text).not.toContain('clip'); + expect(text).not.toContain('agent framework'); + expect(text).not.toContain('recommendation'); + }); +}); diff --git a/frontend/src/app/home/home-page/home-page.ts b/frontend/src/app/home/home-page/home-page.ts new file mode 100644 index 0000000..25a7ebb --- /dev/null +++ b/frontend/src/app/home/home-page/home-page.ts @@ -0,0 +1,65 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, DestroyRef, OnInit, computed, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { RouterLink } from '@angular/router'; +import { catchError, map, of } from 'rxjs'; +import { MovieSummary } from '../../movies/movie.models'; +import { MoviesApi } from '../../movies/movies-api'; + +type HomeLoadState = 'loading' | 'success' | 'empty' | 'error'; + +const homeMovieLimit = 6; +const tmdbImageSize = 'w342'; +const tmdbImageBaseUrl = `https://image.tmdb.org/t/p/${tmdbImageSize}`; + +@Component({ + selector: 'app-home-page', + imports: [CommonModule, RouterLink], + templateUrl: './home-page.html', + styleUrl: './home-page.css', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class HomePage implements OnInit { + private readonly moviesApi = inject(MoviesApi); + private readonly destroyRef = inject(DestroyRef); + + protected readonly loadState = signal('loading'); + protected readonly movies = signal([]); + protected readonly hasMovies = computed(() => this.movies().length > 0); + + public ngOnInit(): void { + this.moviesApi.listMovies({ page: 1, pageSize: homeMovieLimit }).pipe( + map(response => response.items.slice(0, homeMovieLimit)), + catchError(() => { + this.loadState.set('error'); + this.movies.set([]); + return of([]); + }), + takeUntilDestroyed(this.destroyRef) + ).subscribe(items => { + if (this.loadState() === 'error') { + return; + } + + this.movies.set(items); + this.loadState.set(items.length > 0 ? 'success' : 'empty'); + }); + } + + protected detailsLink(movie: MovieSummary): string[] { + return ['/movies', movie.id]; + } + + protected posterImageUrl(posterPath: string): string { + if (posterPath.startsWith('http://') || posterPath.startsWith('https://')) { + return posterPath; + } + + const normalizedPosterPath = posterPath.startsWith('/') ? posterPath : '/' + posterPath; + return tmdbImageBaseUrl + normalizedPosterPath; + } + + protected trackByMovieId(_index: number, movie: MovieSummary): string { + return movie.id; + } +} diff --git a/frontend/src/app/movies/movie-details-page/movie-details-page.css b/frontend/src/app/movies/movie-details-page/movie-details-page.css index 0d71426..908db35 100644 --- a/frontend/src/app/movies/movie-details-page/movie-details-page.css +++ b/frontend/src/app/movies/movie-details-page/movie-details-page.css @@ -82,6 +82,16 @@ color: #cbd5e1; } +.state-title { + margin: 0; + color: #f8fafc; + font-size: clamp(1.75rem, 4vw, 2.5rem); + font-weight: 800; +} + +.state-message { + color: #cbd5e1; +} .hero-actions, .genre-tags { display: flex; @@ -90,8 +100,8 @@ } .genre-tags span { - background: #6366f124; - color: #e0e7ff; + background: #312e81; + color: #f8fafc; padding: .38rem .7rem; font-size: .75rem; } @@ -125,7 +135,10 @@ .hero-synopsis { border: 1px solid #ffffff14; background: #17181db8; - backdrop-filter: blur(12px); + border-radius: .75rem; + max-width: 48rem; + margin-top: 1.25rem; + padding: 1.15rem 1.25rem; } .details-layout { @@ -146,18 +159,11 @@ min-width: 0; } -.hero-synopsis, .insight-panel, .meta-grid div { border-radius: .75rem; } -.hero-synopsis { - max-width: 48rem; - margin-top: 1.25rem; - padding: 1.15rem 1.25rem; -} - .hero-synopsis h2, .insight-panel header { display: flex; @@ -257,7 +263,7 @@ text-transform: uppercase; } -.info-badge-country { border-color: #22c55e40; background: #22c55e24; color: #22c55e; } +.info-badge-country { border-color: #22c55e66; background: #14532d; color: #bbf7d0; } @media (min-width: 760px) { .meta-grid { diff --git a/frontend/src/app/movies/movie-details-page/movie-details-page.html b/frontend/src/app/movies/movie-details-page/movie-details-page.html index 0ec6adf..72440d1 100644 --- a/frontend/src/app/movies/movie-details-page/movie-details-page.html +++ b/frontend/src/app/movies/movie-details-page/movie-details-page.html @@ -1,20 +1,20 @@ -
-
+
+ -

Loading movie details

-
+ Loading movie details + -
+ -

Movie not found

-

We could not find this movie in the catalog.

+ Movie not found + We could not find this movie in the catalog. Back to catalog -
+ diff --git a/frontend/src/app/movies/movie-details-page/movie-details-page.spec.ts b/frontend/src/app/movies/movie-details-page/movie-details-page.spec.ts index e6c32c3..8ccc27c 100644 --- a/frontend/src/app/movies/movie-details-page/movie-details-page.spec.ts +++ b/frontend/src/app/movies/movie-details-page/movie-details-page.spec.ts @@ -103,4 +103,48 @@ describe('MovieDetailsPage', () => { const image = fixture.nativeElement.querySelector('.hero-poster img') as HTMLImageElement; expect(image.src).toBe('https://image.tmdb.org/t/p/w500/p/central.jpg'); }); + + it('should keep absolute poster URL unchanged', () => { + moviesApi.getMovieById.mockReturnValue(of({ + ...details, + posterUrl: 'https://image.tmdb.org/t/p/w500/direct.jpg' + })); + createComponent(); + + const image = fixture.nativeElement.querySelector('.hero-poster img') as HTMLImageElement; + expect(image.src).toBe('https://image.tmdb.org/t/p/w500/direct.jpg'); + }); + + it('should render not-found state when route id is empty', () => { + paramMap.next(convertToParamMap({ id: ' ' })); + createComponent(); + + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.textContent).toContain('Movie not found'); + }); + + it('should expose fallback values for missing metadata helpers', () => { + moviesApi.getMovieById.mockReturnValue(of({ + ...details, + originalTitle: null, + director: null, + synopsis: null, + durationMinutes: null, + ageRating: null, + externalId: null, + posterUrl: null, + genres: [] + })); + createComponent(); + + const component = fixture.componentInstance as unknown as { + metadataCompleteness: () => number; + heroBackgroundStyle: (value: string | null) => string | null; + valueOrFallback: (value: string | number | null | undefined) => string | number; + }; + + expect(component.metadataCompleteness()).toBe(0); + expect(component.heroBackgroundStyle(null)).toBeNull(); + expect(component.valueOrFallback('')).toBe('Not informed'); + }); }); diff --git a/frontend/src/app/movies/movie-details-page/movie-details-page.ts b/frontend/src/app/movies/movie-details-page/movie-details-page.ts index 297eb81..c90e306 100644 --- a/frontend/src/app/movies/movie-details-page/movie-details-page.ts +++ b/frontend/src/app/movies/movie-details-page/movie-details-page.ts @@ -86,7 +86,8 @@ export class MovieDetailsPage implements OnInit { return posterPath; } - return `${tmdbImageBaseUrl}${posterPath.startsWith('/') ? posterPath : `/${posterPath}`}`; + const normalizedPosterPath = posterPath.startsWith('/') ? posterPath : '/' + posterPath; + return tmdbImageBaseUrl + normalizedPosterPath; } protected heroBackgroundStyle(posterPath: string | null): string | null { diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a9b35c1..dd5ce22 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,6 +5,7 @@ import { provideRouter } from '@angular/router'; import { App } from './app/app'; import { LoginPage } from './app/auth/login-page/login-page'; import { CatalogPage } from './app/catalog/catalog-page/catalog-page'; +import { HomePage } from './app/home/home-page/home-page'; import { MovieDetailsPage } from './app/movies/movie-details-page/movie-details-page'; bootstrapApplication(App, { @@ -12,7 +13,8 @@ bootstrapApplication(App, { provideBrowserGlobalErrorListeners(), provideHttpClient(), provideRouter([ - { path: '', component: LoginPage }, + { path: '', component: HomePage }, + { path: 'login', component: LoginPage }, { path: 'catalog', component: CatalogPage }, { path: 'movies/:id', component: MovieDetailsPage } ]), diff --git a/specs/013-view-movie-details/.spec-context.json b/specs/013-view-movie-details/.spec-context.json index d010354..8af1f01 100644 --- a/specs/013-view-movie-details/.spec-context.json +++ b/specs/013-view-movie-details/.spec-context.json @@ -4,7 +4,7 @@ "branch": "013-view-movie-details", "selectedAt": "2026-05-09T00:34:54.614Z", "currentStep": "plan", - "status": "planning", + "status": "completed", "stepHistory": { "plan": { "startedAt": "2026-05-09T00:57:19.273Z", @@ -45,6 +45,16 @@ }, "by": "extension", "at": "2026-05-09T00:57:19.273Z" + }, + { + "step": "plan", + "substep": null, + "from": { + "step": "plan", + "substep": null + }, + "by": "extension", + "at": "2026-05-09T10:30:28.258Z" } ] } \ No newline at end of file diff --git a/specs/013-view-movie-details/tasks.md b/specs/013-view-movie-details/tasks.md index 42cead7..3c3e514 100644 --- a/specs/013-view-movie-details/tasks.md +++ b/specs/013-view-movie-details/tasks.md @@ -21,7 +21,6 @@ - [ ] T002 Create application get-by-id query/handler files under `backend/src/SmartMovieCatalog.Application/Features/Movies/` - [ ] T003 [P] Create API endpoint slice folder `backend/src/SmartMovieCatalog.Api/Features/Movies/GetMovieById/` - [ ] T004 [P] Create frontend details page folder `frontend/src/app/movies/movie-details-page/` - --- ## Phase 2: Foundational (Blocking Prerequisites) diff --git a/specs/017-home-movie-cards/.spec-context.json b/specs/017-home-movie-cards/.spec-context.json new file mode 100644 index 0000000..91f4de6 --- /dev/null +++ b/specs/017-home-movie-cards/.spec-context.json @@ -0,0 +1,36 @@ +{ + "workflow": "speckit", + "specName": "017-home-movie-cards", + "branch": "017-home-movie-cards", + "selectedAt": "2026-05-09T10:30:32.912Z", + "currentStep": "tasks", + "status": "completed", + "stepHistory": { + "tasks": { + "startedAt": "2026-05-09T10:40:00.926Z", + "completedAt": "2026-05-09T10:40:00.926Z" + } + }, + "transitions": [ + { + "step": "tasks", + "substep": null, + "from": { + "step": "plan", + "substep": null + }, + "by": "extension", + "at": "2026-05-09T10:40:00.926Z" + }, + { + "step": "tasks", + "substep": null, + "from": { + "step": "tasks", + "substep": null + }, + "by": "extension", + "at": "2026-05-09T11:01:13.969Z" + } + ] +} \ No newline at end of file diff --git a/specs/017-home-movie-cards/checklists/requirements.md b/specs/017-home-movie-cards/checklists/requirements.md new file mode 100644 index 0000000..a8ce3cc --- /dev/null +++ b/specs/017-home-movie-cards/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Home Movie Cards + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-05-09 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- The spec records one product routing conflict: current frontend documentation says `/` displays login, while issue #17 asks for movie cards on the home page. This is documented as a planning conflict rather than a clarification blocker because a reasonable default exists: preserve authentication access while adding a product-facing home page. diff --git a/specs/017-home-movie-cards/contracts/home-ui.md b/specs/017-home-movie-cards/contracts/home-ui.md new file mode 100644 index 0000000..6729b70 --- /dev/null +++ b/specs/017-home-movie-cards/contracts/home-ui.md @@ -0,0 +1,90 @@ +# UI Contract: Home Movie Cards + +## Routes + +### `/` + +Purpose: Public product home page with a 6-card movie discovery section. + +Expected behavior: + +- Loads up to 6 movie summaries. +- Displays loading, empty, error, and success states. +- Displays movie cards when one or more movies are available. +- Each movie card links to `/movies/{id}`. +- Does not show unsupported V1 AI/search terminology. + +### `/login` + +Purpose: Authentication entry point. + +Expected behavior: + +- Displays the existing login screen. +- Preserves existing login behavior and generic authentication error handling. + +### Existing Routes Preserved + +- `/catalog`: full catalog browsing page. +- `/movies/{id}`: movie details page. + +## Data Request Contract + +Home page uses the existing movie listing behavior through the frontend movie API service. + +Expected request semantics: + +```http +GET /api/movies?page=1&pageSize=6 +``` + +Expected response semantics: + +- `items`: movie summaries to display. +- `page`: current page number. +- `pageSize`: requested page size. +- `totalCount`: total matching movies. +- `totalPages`: total available pages. +- `hasPreviousPage`: previous-page indicator. +- `hasNextPage`: next-page indicator. + +The home page uses `items` for card rendering and does not expose pagination controls. + +## Rendered State Contract + +### Loading + +Condition: movie request is pending. + +Expected UI: + +- A visible loading state in the movie section. +- No stale movie cards. + +### Empty + +Condition: request succeeds with zero movies. + +Expected UI: + +- A visible empty state. +- No card grid. + +### Error + +Condition: request fails. + +Expected UI: + +- A visible generic error state. +- No raw technical error details. + +### Success + +Condition: request succeeds with at least one movie. + +Expected UI: + +- One card per returned movie, up to 6 cards. +- Card content uses available title, release year, genres, director, poster/placeholder, and basic metadata. +- Each card target is `/movies/{id}`. diff --git a/specs/017-home-movie-cards/data-model.md b/specs/017-home-movie-cards/data-model.md new file mode 100644 index 0000000..c86a6e2 --- /dev/null +++ b/specs/017-home-movie-cards/data-model.md @@ -0,0 +1,74 @@ +# Data Model: Home Movie Cards + +## HomeMovieSection + +Represents the movie discovery area on the public home page. + +### Fields + +- `loadState`: current section state. +- `movies`: ordered collection of movie summaries displayed as cards. +- `maxCards`: fixed value of 6 for this feature. + +### Rules + +- Shows loading state while data is being retrieved. +- Shows empty state when the catalog contains no movies. +- Shows error state when data cannot be loaded. +- Shows success state when at least one movie summary is available. +- Displays at most 6 movie summaries. + +## HomeLoadState + +Represents the user-visible state of the home movie section. + +### Values + +- `loading`: data request is pending. +- `success`: at least one movie is available. +- `empty`: no movies are available. +- `error`: movie data cannot be loaded. + +### Transitions + +- Initial state is `loading`. +- `loading` -> `success` when the request returns one or more movies. +- `loading` -> `empty` when the request returns zero movies. +- `loading` -> `error` when the request fails. + +## MovieSummary + +Represents a movie preview returned by the existing movie listing behavior. + +### Fields Used By This Feature + +- `id`: stable movie identifier used for details navigation. +- `title`: primary card title. +- `releaseYear`: visible year metadata. +- `countryCode`: visible basic metadata when available. +- `genres`: visible genre labels when available. +- `director`: visible director metadata when available. +- `posterUrl`: poster image reference when available. + +### Rules + +- `id` and `title` are required for a navigable card. +- Optional fields may be absent and must not break layout. +- `posterUrl` may be absent; card must show a placeholder. + +## MovieCard + +Represents one navigable home card. + +### Fields + +- `movie`: movie summary represented by the card. +- `detailsTarget`: route target for the movie details page. +- `posterDisplay`: either poster image or placeholder. + +### Rules + +- Activating the card navigates to `/movies/{id}`. +- Card must be pointer and keyboard accessible. +- Card text must fit on mobile and desktop without overlapping content. +- Card must not expose unsupported V1 AI/search terminology. diff --git a/specs/017-home-movie-cards/plan.md b/specs/017-home-movie-cards/plan.md new file mode 100644 index 0000000..9231378 --- /dev/null +++ b/specs/017-home-movie-cards/plan.md @@ -0,0 +1,270 @@ +# Implementation Plan: Home Movie Cards + +**Feature Directory**: `specs/017-home-movie-cards` | **Date**: 2026-05-09 | **Spec**: `specs/017-home-movie-cards/spec.md` +**Input**: Feature specification from `specs/017-home-movie-cards/spec.md` + +## Summary + +Implement a public product home page at `/` that displays up to 6 real movie cards from the existing movie listing flow, preserves authentication through `/login`, supports loading/empty/error states, and links every rendered card to the movie details page. + +This plan is frontend-owned for issue #17. Existing backend list and details endpoints are prerequisites; no backend contract or persistence changes are planned unless implementation proves the current listing behavior cannot provide the required movie summaries. + +## Technical Context + +**Language/Version**: TypeScript 5.9 with Angular 21; backend context is ASP.NET Core 10 / C# but no backend implementation is owned here. +**Primary Dependencies**: Angular core/common/router/http, RxJS 7.8, existing `MoviesApi`, existing movie models, local CSS aligned with `frontend/DESIGN.md`. +**Storage**: N/A. Home page state is transient component state; no browser persistence is introduced. +**Testing**: Angular unit/component tests through `npm test -- --watch=false` using Vitest and Angular testing utilities. +**Target Platform**: Browser SPA served through Angular dev server locally and ASP.NET Core SpaProxy/static fallback in integrated runtime. +**Project Type**: Web application frontend feature in the existing monorepo. +**Performance Goals**: Fetch and render at most 6 home movie cards for the initial home surface; avoid duplicate requests for a single home page load. +**Constraints**: No new frontend dependencies, no new UI library, no new design system, no direct `HttpClient` use in components, no unsupported AI/search/recommendation terminology, and login must remain reachable at `/login`. +**Scale/Scope**: One public home page route, one login route adjustment, up to 6 movie cards, focused reusable card extraction if it reduces duplication with catalog. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- Actual architecture is the source of truth: pass. Current source has Angular 21, existing routes for login/catalog/details, `MoviesApi`, and catalog card rendering. +- Boundaries explicit: pass. Implementation stays under `frontend` plus documentation updates; backend work remains prerequisite-only. +- Contracts drive cross-boundary work: pass with dependency. The feature consumes the existing movie listing shape and details route without changing public API contracts. +- Security and secret hygiene: pass. No secrets, token persistence, new auth mechanism, or internal backend details are introduced. +- Small verified changes: pass. Work decomposes into route change, home page, optional card reuse, focused tests, and docs update for routing. +- Frontend design compliance: pass. UI must follow `frontend/DESIGN.md`, local CSS, Material Symbols, and the dark cinematic direction. + +No constitution violations require justification. + +## Project Structure + +### Documentation (this feature) + +```text +specs/017-home-movie-cards/ +|-- spec.md +|-- plan.md +|-- research.md +|-- data-model.md +|-- quickstart.md +|-- contracts/ +| `-- home-ui.md +`-- tasks.md +``` + +### Source Code (repository root) + +```text +frontend/ +`-- src/ + `-- app/ + |-- app.ts + |-- app.html + |-- app.css + |-- app.spec.ts + |-- auth/ + | `-- login-page/ + | |-- login-page.ts + | |-- login-page.html + | |-- login-page.css + | `-- login-page.spec.ts + |-- catalog/ + | `-- catalog-page/ + | |-- catalog-page.ts + | |-- catalog-page.html + | |-- catalog-page.css + | `-- catalog-page.spec.ts + |-- home/ + | `-- home-page/ + | |-- home-page.ts + | |-- home-page.html + | |-- home-page.css + | `-- home-page.spec.ts + |-- movies/ + | |-- movie.models.ts + | |-- movies-api.ts + | |-- movies-api.spec.ts + | `-- movie-details-page/ + `-- shared/ + `-- movie-card/ + |-- movie-card.ts + |-- movie-card.html + |-- movie-card.css + `-- movie-card.spec.ts +``` + +**Structure Decision**: Add a `home/home-page` feature folder for route-specific orchestration. Reuse existing `movies` models/API service. Extract a small shared movie card only if it replaces duplicated card rendering between home and catalog without broad styling or behavior churn; otherwise keep home card markup local and defer extraction. + +## Complexity Tracking + +No complexity violations. + +## Phase 0: Research + +Research output: `specs/017-home-movie-cards/research.md` + +Resolved decisions: + +- `/` becomes the public home page; login moves to `/login`. +- The home page displays up to 6 cards from existing movie listing data. +- Home page loading uses component-local state and does not use URL-backed pagination/search state. +- Home cards link to `/movies/{id}`. +- Components use `MoviesApi`; no component injects `HttpClient`. +- Shared movie card extraction is conditional on reducing real duplication with catalog. + +No `NEEDS CLARIFICATION` items remain. + +## Phase 1: Design & Contracts + +Design outputs: + +- `specs/017-home-movie-cards/data-model.md` +- `specs/017-home-movie-cards/contracts/home-ui.md` +- `specs/017-home-movie-cards/quickstart.md` + +Contract summary: + +- Public route `/` renders home movie cards. +- Public route `/login` renders the existing login screen. +- Home page consumes the existing movie listing API through `MoviesApi` with a request sized to 6 items. +- Home cards target `/movies/{id}`. + +Post-design constitution check: pass. The design does not add dependencies, backend behavior, persistence, external services, new auth storage, or unsupported product terminology. + +## Technical Approach + +1. Add a `HomePage` component under `frontend/src/app/home/home-page`. +2. Update routing so: + - `/` renders `HomePage`. + - `/login` renders `LoginPage`. + - `/catalog` and `/movies/:id` keep current behavior. +3. Update app navigation so users can reach Home, Catalog, and Login as appropriate. +4. Load movie summaries through `MoviesApi.listMovies` using `page=1` and `pageSize=6`. +5. Render home states: + - loading while the request is pending. + - empty when zero movies are returned. + - error when the request fails. + - success when at least one movie is available. +6. Render up to 6 movie cards with title, release year, genres, director, poster/placeholder, and basic metadata. +7. Link each card to `/movies/{id}` and support keyboard activation. +8. Evaluate whether catalog card markup should be extracted into `shared/movie-card`; extract only if the resulting change is smaller and clearer than duplication. +9. Update `docs/FRONTEND.md` to describe `/` as home and `/login` as login. +10. Add focused tests for route behavior, home states, data loading, card count, and details links. + +## Affected Areas + +- `frontend/src/main.ts` +- `frontend/src/app/app.html` +- `frontend/src/app/app.css` +- `frontend/src/app/app.spec.ts` +- `frontend/src/app/home/home-page/*` +- `frontend/src/app/catalog/catalog-page/*` if card extraction is chosen +- `frontend/src/app/shared/movie-card/*` if card extraction is chosen +- `frontend/src/app/movies/movies-api.ts` only if a helper for home list parameters is warranted +- `docs/FRONTEND.md` +- `AGENTS.md` Spec Kit current-plan reference + +## Data Model Changes + +No backend domain, persistence, or public API contract changes. + +Frontend data concepts are documented in `specs/017-home-movie-cards/data-model.md`: + +- `HomeMovieSection` +- `HomeLoadState` +- `MovieSummary` +- `MovieCard` + +## API Changes + +No API changes are owned by issue #17. + +Prerequisite API behavior: + +- `GET /api/movies?page=1&pageSize=6` returns a paged movie summary response. +- Each returned item includes an ID suitable for `/movies/{id}` navigation. +- Optional metadata may be null or empty and must be rendered with safe fallbacks. + +## Frontend Changes + +- Add public home page at `/`. +- Move login route to `/login`. +- Keep `/catalog` as the full browsing surface. +- Keep `/movies/:id` as the details surface. +- Render a bounded 6-card movie section on the home page. +- Show loading, empty, and API error states. +- Keep UI copy free of unsupported terms: RAG, Qdrant, vector search, semantic search, CLIP, agent framework, recommendations, and AI ranking. +- Keep styling aligned with `frontend/DESIGN.md`. + +## Backend Changes + +None for issue #17. + +## Testing Strategy + +- App routing/shell tests: + - root route maps to home. + - `/login` maps to the login page. + - navigation exposes Home/Catalog/Login entry points without breaking existing routes. +- Home page tests: + - calls movie service with `page=1` and `pageSize=6`. + - renders loading state. + - renders empty state. + - renders error state. + - renders fewer than 6 cards when fewer movies exist. + - renders exactly 6 cards when at least 6 movies exist. + - cards link to `/movies/{id}`. + - no prohibited terminology appears in rendered home text. +- Movie card tests if extraction occurs: + - poster image and placeholder behavior. + - metadata fallbacks. + - accessible link label and keyboard-compatible activation. + +Verification: + +- `npm test -- --watch=false` from `frontend` +- `npm run build` from `frontend` + +Backend verification is not required unless implementation expands scope into API contracts or backend code. + +## Deployment Impact + +- No new infrastructure. +- No new frontend dependencies. +- No new backend dependencies. +- ASP.NET Core SPA fallback should serve `/`, `/login`, `/catalog`, and `/movies/{id}` in integrated non-test runtime. + +## Security Considerations + +- `/` and `/catalog` are public catalog discovery surfaces for V1. +- `/login` remains the authentication entry point. +- Do not persist bearer tokens or change auth session storage. +- Treat API response data as untrusted and render through Angular bindings. +- Do not render untrusted HTML. +- Show generic user-facing API failure copy; do not expose raw backend errors. + +## Observability / Logging + +- No backend logging changes. +- No frontend production logging of raw API payloads. +- User-visible states communicate pending, empty, and failure conditions. + +## Risks + +- Routing docs and tests may still assume `/` is login and must be updated together. +- Extracting a shared movie card may create more churn than value if catalog and home card layouts diverge. +- Current catalog image URL normalization contains logic that should stay consistent if a second card surface is added. +- If the backend default ordering is not actually "recently added", home copy must avoid claiming recency beyond what the API guarantees. + +## Rollout Plan + +1. Update routing and docs for `/` home and `/login`. +2. Add home page component and tests for loading/empty/error/success states. +3. Implement home movie loading through `MoviesApi`. +4. Render up to 6 cards and details links. +5. Extract shared card only if it reduces duplication cleanly. +6. Run frontend tests and build. + +## Implementation Decision (T030) + +- Decision date: 2026-05-09. +- Result: shared `movie-card` extraction was not selected. +- Rationale: current home and catalog cards have different structure and visual emphasis; extracting now would increase churn and coupling without clear maintainability gain for issue #17 scope. diff --git a/specs/017-home-movie-cards/quickstart.md b/specs/017-home-movie-cards/quickstart.md new file mode 100644 index 0000000..ae3a1c3 --- /dev/null +++ b/specs/017-home-movie-cards/quickstart.md @@ -0,0 +1,39 @@ +# Quickstart: Home Movie Cards + +## Preconditions + +- The backend movie listing endpoint is available through the existing `/api/movies` behavior. +- The movie details route `/movies/{id}` is available. +- Frontend dependencies are installed in `frontend`. + +## Manual Validation + +1. Start the backend and frontend through the repository's normal local workflow. +2. Open the frontend root route `/`. +3. Verify the page is a public home page and not the login form. +4. Verify the home movie section shows a loading state while requesting data. +5. With at least 6 movies available, verify exactly 6 movie cards render. +6. With fewer than 6 movies available, verify only the available movies render. +7. With no movies available, verify the empty state renders. +8. Simulate an API failure and verify the generic error state renders. +9. Activate a movie card and verify navigation to `/movies/{id}`. +10. Open `/login` and verify the existing login screen still works. +11. Open `/catalog` and `/movies/{id}` and verify existing routes still work. +12. Check mobile and desktop widths for overlapping text or broken card layout. +13. Confirm the home UI does not show unsupported terms: RAG, Qdrant, vector search, semantic search, CLIP, recommendation, AI ranking, or agent framework. + +## Automated Verification + +Run from `frontend`: + +```powershell +npm test -- --watch=false +``` + +Run from `frontend`: + +```powershell +npm run build +``` + +Backend verification is only required if implementation expands scope into backend code or public API contracts. diff --git a/specs/017-home-movie-cards/research.md b/specs/017-home-movie-cards/research.md new file mode 100644 index 0000000..93a0135 --- /dev/null +++ b/specs/017-home-movie-cards/research.md @@ -0,0 +1,66 @@ +# Research: Home Movie Cards + +## Decision: Root route becomes public home + +**Decision**: Route `/` renders the public home page with movie cards, and the existing login screen moves to `/login`. + +**Rationale**: Issue #17 explicitly asks for movie cards on the home page. The clarification session resolved the current documentation conflict by making the root route the product-facing entry surface while preserving authentication access. + +**Alternatives considered**: + +- Keep `/` as login and add `/home`: rejected because it weakens the issue's "home page" requirement. +- Embed movie cards in the login page: rejected because it mixes authentication and catalog discovery responsibilities. + +## Decision: Home page shows up to 6 cards + +**Decision**: The home page requests and displays up to 6 movie summaries. + +**Rationale**: Six cards provide a meaningful preview without duplicating the full catalog page. This also keeps mobile rendering and first-load work bounded. + +**Alternatives considered**: + +- Four cards: rejected because it may underrepresent the catalog on desktop. +- Twelve cards or full first page: rejected because it duplicates the catalog browsing surface. + +## Decision: Use existing movie listing service boundary + +**Decision**: Home page loads movies through the existing typed movie API service and does not call network APIs directly from presentation components. + +**Rationale**: This follows current frontend rules and keeps server communication centralized in typed services. + +**Alternatives considered**: + +- Direct component network calls: rejected by repository frontend rules. +- New home-specific API service: rejected unless implementation discovers distinct home-only behavior. + +## Decision: Keep home state local + +**Decision**: Home loading, empty, error, and success states are local page state. Search, pagination controls, and URL query synchronization remain catalog-page concerns. + +**Rationale**: The home page is a bounded discovery surface, not the full browsing interface. The catalog page already owns full browsing state. + +**Alternatives considered**: + +- Reuse catalog URL query state: rejected because it adds unnecessary complexity and user-visible controls to the home page. +- Add home pagination: rejected as out of scope. + +## Decision: Conditional movie card extraction + +**Decision**: Extract a shared movie card only if it clearly reduces duplication between home and catalog without broad styling churn. + +**Rationale**: The issue suggests a reusable card when it will also be used by catalog. The catalog already has card markup; extraction is useful only if the shared component remains small and preserves each page's visual needs. + +**Alternatives considered**: + +- Always extract first: rejected because it can enlarge the change and force catalog refactoring before proving reuse value. +- Never extract: rejected because duplicate card behavior may create drift in navigation, poster fallback, and metadata display. + +## Decision: Avoid unsupported product terminology + +**Decision**: Home page copy must not include unsupported terms such as RAG, Qdrant, vector search, semantic search, CLIP, recommendation, AI ranking, or agent framework. + +**Rationale**: The issue and existing frontend documentation explicitly restrict these V1 claims. The feature is catalog discovery, not AI discovery. + +**Alternatives considered**: + +- Use AI-themed marketing copy: rejected because it overstates current product behavior. diff --git a/specs/017-home-movie-cards/spec.md b/specs/017-home-movie-cards/spec.md new file mode 100644 index 0000000..37ef572 --- /dev/null +++ b/specs/017-home-movie-cards/spec.md @@ -0,0 +1,142 @@ +# Feature Specification: Home Movie Cards + +**Feature Branch**: `017-home-movie-cards` +**Created**: 2026-05-09 +**Status**: Draft +**Input**: GitHub issue #17: "Movie cards on the home page" - https://github.com/marciomyst/SmartMovieCatalog/issues/17 + +## Clarifications + +### Session 2026-05-09 + +- Q: How should the product home page coexist with the current login screen at the root route? -> A: Make `/` the public home page with movie cards, and move login to `/login`. +- Q: How many movie cards should the home page show initially? -> A: Show 6 movie cards. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Browse Movies From Home (Priority: P1) + +As a visitor opening the application, I want the home page to show real movie cards so that I can immediately discover catalog content without first navigating to a separate browsing page. + +**Why this priority**: This is the core value of the feature. The current product entry experience does not expose catalog content on the home page. + +**Independent Test**: Seed or mock catalog data, open the home page, and verify that movie cards are rendered with real movie summary data and no scaffold/sample-only content. + +**Acceptance Scenarios**: + +1. **Given** movies exist in the catalog, **When** a visitor opens the home page, **Then** the visitor sees a responsive section of movie cards populated from catalog data. +2. **Given** a movie has title, release year, genres, director, and poster data, **When** its card is shown on the home page, **Then** those available values are visible without exposing unsupported product claims. +3. **Given** a movie has no poster image, **When** its card is shown, **Then** a polished poster placeholder is displayed instead of a broken image. + +--- + +### User Story 2 - Understand Home Page States (Priority: P2) + +As a visitor, I want clear loading, empty, and error states so that I understand whether the catalog is loading, empty, or temporarily unavailable. + +**Why this priority**: The home page must remain credible and usable when data is unavailable or delayed. + +**Independent Test**: Mock loading, empty, and failed catalog responses, then verify that each state renders a distinct user-facing message and does not display stale or misleading movie cards. + +**Acceptance Scenarios**: + +1. **Given** catalog data is being requested, **When** the home page is loading, **Then** a loading state is displayed in the movie section. +2. **Given** the catalog contains no movies, **When** the home page finishes loading, **Then** an empty state explains that no movies are available yet. +3. **Given** the catalog request fails, **When** the home page finishes loading, **Then** an error state is displayed without exposing internal error details. + +--- + +### User Story 3 - Open Movie Details From Home (Priority: P3) + +As a visitor, I want to click a movie card on the home page so that I can open the movie details page for that movie. + +**Why this priority**: Details navigation makes the home page cards actionable and connects this feature to the catalog exploration flow. + +**Independent Test**: Render home page cards with movie IDs, activate a card, and verify that the application navigates to the corresponding movie details page. + +**Acceptance Scenarios**: + +1. **Given** movie cards are visible on the home page, **When** a visitor activates a card, **Then** the visitor is taken to that movie's details page. +2. **Given** a visitor uses keyboard navigation, **When** focus reaches a movie card, **Then** the card can be activated without requiring a mouse. + +### Edge Cases + +- Catalog data is empty. +- Catalog data is slow to load. +- Catalog data request fails. +- A movie summary contains missing optional metadata such as director, genres, or poster URL. +- A poster URL fails to load or is absent. +- A long movie title, director name, or genre list must not break the card layout. +- A visitor opens the home page on a narrow mobile viewport. +- The current root route is already used by the login screen; the login screen must move to `/login` so `/` can become the public home page. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The application MUST provide a product-facing home page surface that can display movie catalog content. +- **FR-002**: The home page MUST include a movie card section populated from real catalog movie summary data. +- **FR-003**: The home page movie section MUST show 6 movie cards when at least 6 movies are available. +- **FR-004**: The home page movie section MUST display fewer than 6 movie cards when fewer than 6 movies are available but at least one movie exists. +- **FR-005**: The home page movie section MUST display available card content for each movie, including title, release year, genres, director, poster image or poster placeholder, and basic metadata. +- **FR-006**: The home page movie section MUST display a loading state while movie data is being retrieved. +- **FR-007**: The home page movie section MUST display an empty state when no movies are available. +- **FR-008**: The home page movie section MUST display an error state when movie data cannot be loaded. +- **FR-009**: Each movie card MUST provide navigation to that movie's details page. +- **FR-010**: Movie card navigation MUST support pointer and keyboard activation. +- **FR-011**: The home page movie section MUST remain responsive across desktop and mobile viewports. +- **FR-012**: The home page UI MUST follow the existing dark cinematic product direction. +- **FR-013**: The home page UI MUST NOT expose unsupported V1 terms or claims such as RAG, Qdrant, vector search, semantic search, CLIP, recommendation, or agent framework. +- **FR-014**: Presentation components MUST use the established frontend data access boundary for catalog data and MUST NOT own direct network communication. +- **FR-015**: The root route `/` MUST display the public home page with movie cards. +- **FR-016**: The login screen MUST remain reachable through `/login`. +- **FR-017**: Reusable movie card behavior SHOULD be shared with other catalog surfaces when doing so avoids meaningful duplication without broad refactoring. + +### Key Entities + +- **Movie Summary**: A catalog item preview shown on a card. Key visible attributes include title, release year, genres, director, country or similar basic metadata, poster image reference, and identifier for details navigation. +- **Home Movie Section**: The product-facing home page area that presents movie summaries and its loading, empty, and error states. +- **Movie Card**: A navigable visual representation of one movie summary, including poster or placeholder and available metadata. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: A visitor can open the home page and see catalog movie content or a clear state message in under 3 seconds under normal local development conditions. +- **SC-002**: The home page shows 6 movie cards when at least 6 movies are available. +- **SC-003**: 100% of rendered home page movie cards navigate to the matching details page when activated. +- **SC-004**: The home page movie section presents loading, empty, and error states with distinct user-facing messages. +- **SC-005**: The home page movie section remains usable at common mobile and desktop viewport widths without overlapping text or broken card layout. +- **SC-006**: No unsupported V1 AI, RAG, vector, semantic search, CLIP, recommendation, Qdrant, or agent terminology appears in the home page card experience. + +## Assumptions + +- "Home page" means the public root route `/`, not the existing catalog page. +- The current catalog listing behavior provides the movie summary data needed for home page cards. +- The default catalog ordering is acceptable for "recently added" display unless a dedicated recent-movies behavior is defined later. +- The initial home page movie section shows up to 6 movies rather than full catalog pagination. +- The movie details page route from issue #13 is available for card navigation. +- The catalog page from issue #18 remains available as the full browsing surface. +- Authentication remains reachable through `/login`. + +## Dependencies + +- Existing movie listing data and movie summary shape. +- Existing movie details route backed by issue #13. +- Existing catalog UI behavior from issue #18 that already displays movie cards and state handling. +- Existing frontend visual system and dark cinematic design direction. + +## Conflicts + +- `docs/FRONTEND.md` currently states that `/` displays the authentication login page, while issue #17 asks for movie cards on the home page. This feature resolves the conflict by making `/` the public home page and moving login to `/login`. + +## Out of Scope + +- Poster upload. +- Flip-card AI analysis back side. +- AI analyzed badges. +- Recommendations. +- Infinite scrolling. +- Advanced filters. +- Authentication redesign. +- Backend contract changes unless the existing movie listing behavior is insufficient for home page cards. diff --git a/specs/017-home-movie-cards/tasks.md b/specs/017-home-movie-cards/tasks.md new file mode 100644 index 0000000..9ec2a83 --- /dev/null +++ b/specs/017-home-movie-cards/tasks.md @@ -0,0 +1,201 @@ +# Tasks: Home Movie Cards + +**Input**: Design documents from `specs/017-home-movie-cards/` +**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/home-ui.md`, `quickstart.md` + +**Tests**: Included because the specification defines independent tests for each user story and the frontend already has component/API test structure. + +**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (US1, US2, US3) +- Each task includes exact file paths + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Establish route ownership and documentation prerequisites for the feature. + +- [X] T001 Update routing in `frontend/src/main.ts` so `/` maps to the new home page and `/login` maps to the existing login page. +- [X] T002 Update app navigation labels/links in `frontend/src/app/app.html` for Home, Catalog, and Login routes. +- [X] T003 [P] Update frontend routing documentation in `docs/FRONTEND.md` to describe `/`, `/login`, `/catalog`, and `/movies/:id`. +- [X] T004 [P] Update shell route/navigation expectations in `frontend/src/app/app.spec.ts`. + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Create the home feature shell that all user stories build on. + +**CRITICAL**: No user story work should begin until this phase is complete. + +- [X] T005 Create `frontend/src/app/home/home-page/home-page.ts` with a standalone home page component shell and imports needed for template rendering. +- [X] T006 Create `frontend/src/app/home/home-page/home-page.html` with semantic page structure and an empty movie section placeholder. +- [X] T007 Create `frontend/src/app/home/home-page/home-page.css` aligned with `frontend/DESIGN.md` dark cinematic tokens. +- [X] T008 Create `frontend/src/app/home/home-page/home-page.spec.ts` with test setup and mocked `MoviesApi` boundary. + +**Checkpoint**: Home route exists and can render an empty shell without movie data. + +--- + +## Phase 3: User Story 1 - Browse Movies From Home (Priority: P1) MVP + +**Goal**: Visitors opening `/` see up to 6 real movie cards from catalog data. + +**Independent Test**: Mock catalog data, open the home page, and verify 6 cards render when at least 6 movies exist, fewer cards render when fewer movies exist, and card content uses real movie summary data. + +### Tests for User Story 1 + +- [X] T009 [P] [US1] Add home page success-state tests in `frontend/src/app/home/home-page/home-page.spec.ts` for rendering exactly 6 cards when at least 6 movies are returned. +- [X] T010 [P] [US1] Add home page reduced-result tests in `frontend/src/app/home/home-page/home-page.spec.ts` for rendering fewer than 6 cards when fewer movies are returned. +- [X] T011 [P] [US1] Add poster image and poster placeholder tests in `frontend/src/app/home/home-page/home-page.spec.ts`. +- [X] T012 [P] [US1] Add prohibited-terminology assertion in `frontend/src/app/home/home-page/home-page.spec.ts` for home page rendered text. + +### Implementation for User Story 1 + +- [X] T013 [US1] Inject and call `MoviesApi.listMovies({ page: 1, pageSize: 6 })` in `frontend/src/app/home/home-page/home-page.ts`. +- [X] T014 [US1] Add home movie list state and success mapping in `frontend/src/app/home/home-page/home-page.ts`. +- [X] T015 [US1] Render movie cards with title, release year, country/basic metadata, director, genres, and poster/placeholder in `frontend/src/app/home/home-page/home-page.html`. +- [X] T016 [US1] Style the home movie grid and card visuals in `frontend/src/app/home/home-page/home-page.css`. +- [X] T017 [US1] Add or reuse poster URL normalization in `frontend/src/app/home/home-page/home-page.ts` without introducing direct `HttpClient` usage. + +**Checkpoint**: User Story 1 is functional and independently testable at `/`. + +--- + +## Phase 4: User Story 2 - Understand Home Page States (Priority: P2) + +**Goal**: Visitors see clear loading, empty, and error states for the home movie section. + +**Independent Test**: Mock pending, empty, and failed catalog responses, then verify each state renders a distinct message and no stale cards. + +### Tests for User Story 2 + +- [X] T018 [P] [US2] Add loading-state test in `frontend/src/app/home/home-page/home-page.spec.ts`. +- [X] T019 [P] [US2] Add empty-state test in `frontend/src/app/home/home-page/home-page.spec.ts`. +- [X] T020 [P] [US2] Add error-state test in `frontend/src/app/home/home-page/home-page.spec.ts`. + +### Implementation for User Story 2 + +- [X] T021 [US2] Add `loading`, `success`, `empty`, and `error` load states in `frontend/src/app/home/home-page/home-page.ts`. +- [X] T022 [US2] Render loading, empty, and error state blocks in `frontend/src/app/home/home-page/home-page.html`. +- [X] T023 [US2] Style state blocks in `frontend/src/app/home/home-page/home-page.css` with accessible contrast and no layout shift. +- [X] T024 [US2] Ensure failed requests show generic error copy and do not render stale cards in `frontend/src/app/home/home-page/home-page.ts`. + +**Checkpoint**: User Story 2 works independently with mocked API states. + +--- + +## Phase 5: User Story 3 - Open Movie Details From Home (Priority: P3) + +**Goal**: Every rendered home movie card navigates to the matching movie details page. + +**Independent Test**: Render home cards with movie IDs, activate a card, and verify the generated route target is `/movies/{id}`. + +### Tests for User Story 3 + +- [X] T025 [P] [US3] Add details-link target tests in `frontend/src/app/home/home-page/home-page.spec.ts`. +- [X] T026 [P] [US3] Add keyboard/pointer accessible card semantics test in `frontend/src/app/home/home-page/home-page.spec.ts`. + +### Implementation for User Story 3 + +- [X] T027 [US3] Add `detailsLink(movie)` route target helper in `frontend/src/app/home/home-page/home-page.ts`. +- [X] T028 [US3] Wrap each rendered home card with router navigation to `/movies/{id}` in `frontend/src/app/home/home-page/home-page.html`. +- [X] T029 [US3] Add accessible card labels and focus-visible styling in `frontend/src/app/home/home-page/home-page.html` and `frontend/src/app/home/home-page/home-page.css`. + +**Checkpoint**: User Story 3 works independently and all home cards navigate to details. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Reduce duplication, validate contracts, and complete feature verification. + +- [X] T030 Evaluate catalog/home card duplication and decide whether to extract shared card behavior; document decision in `specs/017-home-movie-cards/plan.md` if implementation differs from the conditional plan. +- [X] T031 [P] If extraction is chosen, create shared card component files under `frontend/src/app/shared/movie-card/`. (N/A: extraction not selected in T030) +- [X] T032 If extraction is chosen, update `frontend/src/app/catalog/catalog-page/catalog-page.html` and `frontend/src/app/catalog/catalog-page/catalog-page.ts` to use the shared card without changing catalog behavior. (N/A: extraction not selected in T030) +- [X] T033 If extraction is chosen, update catalog card tests in `frontend/src/app/catalog/catalog-page/catalog-page.spec.ts`. (N/A: extraction not selected in T030) +- [X] T034 [P] Run quickstart validation steps from `specs/017-home-movie-cards/quickstart.md`. +- [X] T035 Run frontend tests with `npm test -- --watch=false` from `frontend`. +- [X] T036 Run frontend build with `npm run build` from `frontend`. + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies. +- **Foundational (Phase 2)**: Depends on Setup completion and blocks all user stories. +- **User Story 1 (Phase 3)**: Depends on Foundational phase; MVP scope. +- **User Story 2 (Phase 4)**: Depends on Foundational phase and can be tested with mocked data, but integrates most naturally after US1 state wiring. +- **User Story 3 (Phase 5)**: Depends on card rendering from US1. +- **Polish (Phase 6)**: Depends on selected user stories being complete. + +### User Story Dependencies + +- **US1 Browse Movies From Home**: MVP; no dependency on US2 or US3. +- **US2 Understand Home Page States**: Can be developed after foundation, but final integration uses the loading pipeline introduced by US1. +- **US3 Open Movie Details From Home**: Depends on rendered cards from US1. + +### Within Each User Story + +- Tests should be written before implementation. +- Component state before template integration. +- Template integration before CSS polish. +- Story checkpoint should pass before moving to next priority. + +## Parallel Opportunities + +- T003 and T004 can run in parallel with route setup. +- T005, T006, T007, and T008 touch different new files and can be split after route imports are known. +- T009 through T012 can be written in parallel before US1 implementation. +- T018 through T020 can be written in parallel before US2 implementation. +- T025 and T026 can be written in parallel before US3 implementation. +- T031 can run in parallel with quickstart review if card extraction is selected and catalog files are not being edited simultaneously. + +## Parallel Example: User Story 1 + +```text +Task: "Add home page success-state tests in frontend/src/app/home/home-page/home-page.spec.ts" +Task: "Add poster image and poster placeholder tests in frontend/src/app/home/home-page/home-page.spec.ts" +Task: "Add prohibited-terminology assertion in frontend/src/app/home/home-page/home-page.spec.ts" +``` + +## Parallel Example: User Story 2 + +```text +Task: "Add loading-state test in frontend/src/app/home/home-page/home-page.spec.ts" +Task: "Add empty-state test in frontend/src/app/home/home-page/home-page.spec.ts" +Task: "Add error-state test in frontend/src/app/home/home-page/home-page.spec.ts" +``` + +## Parallel Example: User Story 3 + +```text +Task: "Add details-link target tests in frontend/src/app/home/home-page/home-page.spec.ts" +Task: "Add keyboard/pointer accessible card semantics test in frontend/src/app/home/home-page/home-page.spec.ts" +``` + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1 setup. +2. Complete Phase 2 home shell. +3. Complete Phase 3 movie loading and card rendering. +4. Validate US1 independently at `/`. + +### Incremental Delivery + +1. US1: Home displays real movie cards. +2. US2: Add loading, empty, and error state resilience. +3. US3: Add details navigation and accessible activation. +4. Polish: Extract shared card only if it improves maintainability. + +### Verification + +1. Run `npm test -- --watch=false` from `frontend`. +2. Run `npm run build` from `frontend`. +3. Run backend checks only if implementation expands into backend/API files.