Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
677c35a
chore(mews): app scaffolding w/ Next.js and Tailwind for movie app
Kayode-Olumo Oct 23, 2025
96c9b9f
merge: setup app template for Mews movie app
Kayode-Olumo Oct 23, 2025
1a217e3
chore: tailwind and styling setup
Kayode-Olumo Oct 23, 2025
e52d8e8
feat(mews): updated the TMDB types (search, detail, credits)
Kayode-Olumo Oct 24, 2025
f8633c4
chore: clean up
Kayode-Olumo Oct 24, 2025
c373e6a
chore(mews): added pr template
Kayode-Olumo Oct 24, 2025
95a0ada
chore(mews): clean up - removed space
Kayode-Olumo Oct 24, 2025
af3166d
Merge pull request #1 from Kayode-Olumo/feat/constants-types
Kayode-Olumo Oct 24, 2025
88eb650
feat(mews/api): created proxy endpoint for search, movie details and …
Kayode-Olumo Oct 24, 2025
dd02681
Merge pull request #2 from Kayode-Olumo/feat/created-proxy-endpoints
Kayode-Olumo Oct 24, 2025
a91a567
fix(mews): revised and debugged api implementation
Kayode-Olumo Oct 24, 2025
4ea8a00
chore(mews): cobebase clean up
Kayode-Olumo Oct 24, 2025
09fd19e
Merge pull request #3 from Kayode-Olumo/feat/movie-service-layer
Kayode-Olumo Oct 24, 2025
085b34e
refactor: reorganise project structure, standardise naming convention…
Kayode-Olumo Oct 25, 2025
c55a62d
refactor: further codebase clean up - removal of unused files
Kayode-Olumo Oct 25, 2025
99f667f
chore(mews): standardise TMDB_ACCESS_TOKEN to match docs, fix route p…
Kayode-Olumo Oct 25, 2025
ee3e823
feat: add constants module for TMDB URLs, sizes, and defaults as a si…
Kayode-Olumo Oct 25, 2025
acc6d96
refactor: simplified the import for types
Kayode-Olumo Oct 25, 2025
7dbc642
refactor: move TMDB server client to services/ (no behavior change)
Kayode-Olumo Oct 25, 2025
06aa939
chore(mews): styling clean up
Kayode-Olumo Oct 25, 2025
0b730da
chore(mews): styling clean up remove unused variables
Kayode-Olumo Oct 25, 2025
b182487
Merge pull request #4 from Kayode-Olumo/refactor/project-structure-an…
Kayode-Olumo Oct 25, 2025
6d47219
test: add debounce integration test for HomePage search behaviour
Kayode-Olumo Oct 25, 2025
68e2918
test: add HomePage debounce test
Kayode-Olumo Oct 25, 2025
a911c22
Merge pull request #5 from Kayode-Olumo/test/search-debounce-logic
Kayode-Olumo Oct 25, 2025
9929fb0
chore: clean up
Kayode-Olumo Oct 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## Summary
Briefly explain what this PR does and what part of the feature or project it covers.
Keep it conversational but clear — think of it as how you’d describe the work to a teammate.

---

## Motivation
Why was this change made?
Explain the context, problem, or goal this work addresses.

---

## Implementation
Describe what was done at a high level.
Include any key changes to structure, new components, API calls, or refactors that are worth noting.

---

## How to Test
List the steps to test or review the changes locally:
1. Commands to run
2. What to look for or confirm
3. Any environment setup needed

---

## Tasks Completed
- [ ] Created or updated constants and types
- [ ] Added API routes for TMDB (search, detail, credits)
- [ ] Built services layer for data fetching
- [ ] Implemented search view with pagination and debounce
- [ ] Added detail view with director and top cast
- [ ] Added loading, empty, and error states
- [ ] Updated README and environment example
- [ ] General UI polish and responsive updates

---

## Notes
Add anything that might be helpful for reviewers to know — decisions made, trade-offs, or things you’d improve if you had more time.
41 changes: 41 additions & 0 deletions jobs/Frontend/mews-movie-app-kayode-olumo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
36 changes: 36 additions & 0 deletions jobs/Frontend/mews-movie-app-kayode-olumo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import HomePage from "@/app/page";

jest.mock("@/lib/services/movieService", () => ({
searchMovies: jest.fn(),
}));

describe("HomePage search debounce", () => {
let mockSearchMovies: jest.MockedFunction<any>;

beforeEach(async () => {
const { searchMovies } = await import("@/lib/services/movieService");
mockSearchMovies = searchMovies as jest.MockedFunction<any>;
mockSearchMovies.mockResolvedValue({
results: [{ id: 1, title: "Test Movie" }],
total_pages: 1,
});
jest.useRealTimers();
});

afterEach(() => {
jest.clearAllMocks();
});

it("calls searchMovies once after debounce with the final query", async () => {
render(<HomePage />);

await Promise.resolve();
mockSearchMovies.mockClear();

const input = screen.getByPlaceholderText(/search for movies/i);
await userEvent.type(input, "test");

await new Promise((r) => setTimeout(r, 400));

expect(mockSearchMovies).toHaveBeenCalledTimes(1);
expect(mockSearchMovies).toHaveBeenCalledWith("test", 1);
}, 15000);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { tmdbGet } from "@/services/tmdb";

export async function GET(
_req: Request,
{ params }: { params: { id: string } }
) {
try {
const data = await tmdbGet(`/movie/${params.id}?language=en-US`);
return NextResponse.json(data);
} catch (error) {
console.error("Error fetching movie details:", error);
return NextResponse.json({ error: "Failed to fetch movie details" }, { status: 500 });
}
}
28 changes: 28 additions & 0 deletions jobs/Frontend/mews-movie-app-kayode-olumo/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { type NextRequest, NextResponse } from "next/server"
import { tmdbGet } from "@/services/tmdb"

export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get("query") || ""
const page = searchParams.get("page") || "1"

try {
const params = new URLSearchParams({
page: page,
})

if (query.trim()) {
params.append("query", query)
}

const endpoint = query.trim()
? `/search/movie?${params}`
: `/movie/popular?${params}`

const data = await tmdbGet(endpoint)
return NextResponse.json(data)
} catch (error) {
console.error("Error in search route:", error)
return NextResponse.json({ error: "Failed to fetch movies" }, { status: 500 })
}
}
Binary file not shown.
65 changes: 65 additions & 0 deletions jobs/Frontend/mews-movie-app-kayode-olumo/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
@import "tailwindcss";
@import "tw-animate-css";

@custom-variant dark (&:is(.dark *));

:root {
--background: #fafafa;
--foreground: #171717;
--primary: #171717;
--primary-foreground: #fafafa;
--secondary: #f4f4f4;
--muted-foreground: #737373;
--accent: #f4f4f4;
--accent-foreground: #171717;
--destructive: #ef4444;
--destructive-foreground: #fafafa;
--border: #e4e4e7;
--input: #f4f4f4;
--ring: #171717;
--radius: 0.75rem;
}

.dark {
--background: #0a0a0a;
--foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #0a0a0a;
--secondary: #262626;
--muted-foreground: #a3a3a3;
--accent: #262626;
--accent-foreground: #fafafa;
--destructive: #dc2626;
--destructive-foreground: #fafafa;
--border: #262626;
--input: #262626;
--ring: #525252;
}

@theme inline {
--font-sans: "Geist", "Geist Fallback";
--font-serif: "Playfair Display", "Georgia", serif;
--font-mono: "Geist Mono", "Geist Mono Fallback";
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}

@layer base {
* {
@apply outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
28 changes: 28 additions & 0 deletions jobs/Frontend/mews-movie-app-kayode-olumo/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type React from "react"
import type { Metadata } from "next"
import { Geist, Geist_Mono } from 'next/font/google'
import { Playfair_Display } from 'next/font/google'
import "./globals.css"

const _geist = Geist({ subsets: ["latin"] })
const _geistMono = Geist_Mono({ subsets: ["latin"] })
const _playfair = Playfair_Display({ subsets: ["latin"] })

export const metadata: Metadata = {
title: "flixDB - Movie Database",
description: "Search and discover movies",
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={`font-sans antialiased`}>
{children}
</body>
</html>
)
}
54 changes: 54 additions & 0 deletions jobs/Frontend/mews-movie-app-kayode-olumo/app/movie/[id]/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"use client"

import { useEffect } from "react"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { ArrowLeft, AlertCircle } from "lucide-react"

export default function MovieError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error("Movie page error:", error)
}, [error])

return (
<div className="min-h-screen bg-background">
<header className="border-b bg-background sticky top-0 z-10 backdrop-blur-sm bg-background/95">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<Link href="/">
<Button variant="ghost" className="gap-2">
<ArrowLeft className="h-4 w-4" />
<span className="hidden sm:inline">Back to Movies</span>
<span className="sm:hidden">Back</span>
</Button>
</Link>
</div>
</header>

<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8 lg:py-12">
<div className="text-center">
<AlertCircle className="h-16 w-16 text-destructive mx-auto mb-6" />
<h1 className="text-3xl font-bold mb-4">Movie Not Found</h1>
<p className="text-muted-foreground mb-8 max-w-md mx-auto">
We couldn't find the movie you're looking for. It might not exist or there was an error loading it.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button onClick={reset} variant="outline">
Try Again
</Button>
<Link href="/">
<Button>
Browse Movies
</Button>
</Link>
</div>
</div>
</main>
</div>
)
}
Loading