├── .env.example ├── pnpm-workspace.yaml ├── public ├── robots.txt ├── .DS_Store ├── movies.png ├── movies.webp ├── stars.webp ├── movies-sm.webp ├── social-card.png ├── stars-filled.webp ├── tmdb2.svg └── tmdb.svg ├── vercel.json ├── server └── api │ └── index.ts ├── .npmrc ├── eslint.config.js ├── proxy ├── .env.example ├── routes │ ├── index.ts │ ├── ipx │ │ └── [...path].ts │ └── tmdb │ │ └── [...path].ts ├── tsconfig.json ├── package.json ├── nitro.config.ts └── README.md ├── i18n.config.ts ├── tests ├── unit │ ├── config.ts │ └── mocks.ts └── e2e │ └── homapage.spec.ts ├── components ├── video │ ├── Grid.vue │ ├── Card.vue │ └── Card.nuxt.test.ts ├── media │ ├── Grid.vue │ ├── Videos.vue │ ├── Overview.vue │ ├── Details.vue │ ├── Card.vue │ ├── Photos.vue │ ├── AutoLoadGrid.vue │ ├── Hero.vue │ └── Info.vue ├── carousel │ ├── Items.vue │ ├── AutoQuery.vue │ └── Base.vue ├── StarsRate.vue ├── photo │ ├── Card.vue │ ├── Card.nuxt.test.ts │ ├── Modal.vue │ └── Modal.nuxt.test.ts ├── LanguageSwitcher.vue ├── person │ ├── Credits.vue │ ├── Photos.vue │ ├── Card.vue │ ├── CreditsList.vue │ ├── Details.vue │ ├── Info.vue │ ├── Photos.nuxt.test.ts │ ├── Details.nuxt.test.ts │ ├── Credits.nuxt.test.ts │ ├── Card.nuxt.test.ts │ ├── Info.nuxt.test.ts │ └── CreditsList.nuxt.test.ts ├── IframeModal.vue ├── NavBar.vue ├── StarsRate.nuxt.test.ts ├── TheFooter.vue ├── icon │ ├── IconTMDB.vue │ ├── IconNuxt3.vue │ ├── IconVercel.vue │ └── IconNuxt.vue └── ExternalLinks.vue ├── tsconfig.json ├── .vscode └── settings.json ├── .gitignore ├── composables ├── nuxt.ts ├── item.ts ├── nuxt.nuxt.test.ts ├── utils.ts ├── utils.nuxt.test.ts ├── tmdb.ts └── item.nuxt.test.ts ├── constants ├── images.ts ├── lists.ts └── languages.ts ├── plugins └── scroll.client.ts ├── middleware └── disable-vue-transitions.global.ts ├── vitest.config.ts ├── .github └── workflows │ ├── provenance.yml │ ├── tests-unit.yml │ └── tests-e2e.yml ├── pages ├── person │ └── [id].vue ├── [type] │ ├── category │ │ └── [query].vue │ ├── [id].vue │ └── index.vue ├── genre │ └── [no] │ │ ├── tv.vue │ │ └── movie.vue ├── index.vue └── search.vue ├── error.vue ├── LICENSE ├── unocss.config.ts ├── app.vue ├── playwright.config.ts ├── internationalization ├── zh-CN.json ├── ja.json ├── it.json ├── en.json ├── fa-IR.json ├── de-DE.json ├── ru-RU.json ├── vi.json ├── fr-FR.json ├── es-ES.json ├── uk-UA.json ├── pt-PT.json └── pt-BR.json ├── README.md ├── package.json ├── types.ts └── nuxt.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | BASE_URL=http://localhost:3000 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - proxy 3 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /public/movies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/movies.png -------------------------------------------------------------------------------- /public/movies.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/movies.webp -------------------------------------------------------------------------------- /public/stars.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/stars.webp -------------------------------------------------------------------------------- /server/api/index.ts: -------------------------------------------------------------------------------- 1 | export default defineEventHandler(() => 'Nuxt Movies API') 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | shell-emulator=true 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu() 4 | -------------------------------------------------------------------------------- /public/movies-sm.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/movies-sm.webp -------------------------------------------------------------------------------- /public/social-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/social-card.png -------------------------------------------------------------------------------- /public/stars-filled.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt/movies/HEAD/public/stars-filled.webp -------------------------------------------------------------------------------- /proxy/.env.example: -------------------------------------------------------------------------------- 1 | # https://developers.themoviedb.org/3/getting-started/introduction 2 | TMDB_API_KEY= 3 | -------------------------------------------------------------------------------- /i18n.config.ts: -------------------------------------------------------------------------------- 1 | export default defineI18nConfig(() => { 2 | return { 3 | fallbackLocale: 'en', 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /tests/unit/config.ts: -------------------------------------------------------------------------------- 1 | const config = useRuntimeConfig() 2 | 3 | export const baseUrl = config.public.apiBaseUrl 4 | -------------------------------------------------------------------------------- /components/video/Grid.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /components/media/Grid.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /proxy/routes/index.ts: -------------------------------------------------------------------------------- 1 | export default eventHandler(() => { 2 | return 'Nuxt Movies Proxy: Learn more' 3 | }) 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true 5 | }, 6 | "exclude": [ 7 | "proxy/**" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "TMDB" 4 | ], 5 | "eslint.useFlatConfig": true, 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false 8 | } 9 | -------------------------------------------------------------------------------- /proxy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nitro/types/tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "allowSyntheticDefaultImports": true, 6 | "skipLibCheck": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .nuxt 6 | .env 7 | .nitro 8 | .DS_Store 9 | .idea 10 | .vercel 11 | 12 | coverage 13 | test-results 14 | /playwright-report/ 15 | /blob-report/ 16 | /playwright/.cache/ 17 | -------------------------------------------------------------------------------- /composables/nuxt.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from 'vue' 2 | 3 | export function useRouteParam(name: string, init?: T): ComputedRef { 4 | const route = useRoute() 5 | return computed(() => route.params[name] as any ?? init) 6 | } 7 | -------------------------------------------------------------------------------- /constants/images.ts: -------------------------------------------------------------------------------- 1 | export const YOUTUBE_THUMBNAIL_QUALITY_NAME = 'maxresdefault.jpg' 2 | 3 | export const TMDB_IMAGE_BASE_THUMBNAIL = 'https://image.tmdb.org/t/p/original' 4 | export const TMDB_IMAGE_BASE_ORIGINAL = 'https://image.tmdb.org/t/p/original' 5 | -------------------------------------------------------------------------------- /plugins/scroll.client.ts: -------------------------------------------------------------------------------- 1 | import { createRouterScroller } from 'vue-router-better-scroller' 2 | 3 | export default defineNuxtPlugin(({ vueApp }) => { 4 | vueApp.use(createRouterScroller({ 5 | selectors: { 6 | '#app-scroller': true, 7 | }, 8 | })) 9 | }) 10 | -------------------------------------------------------------------------------- /middleware/disable-vue-transitions.global.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware((to) => { 2 | if (typeof document !== 'undefined' && !document.startViewTransition) 3 | return 4 | 5 | // Disable built-in Vue transitions 6 | // to.meta.pageTransition = false 7 | to.meta.layoutTransition = false 8 | }) 9 | -------------------------------------------------------------------------------- /proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "PORT=3001 nitropack dev", 5 | "prepare": "nitropack prepare", 6 | "build": "nitropack build" 7 | }, 8 | "devDependencies": { 9 | "nitropack": "^2.9.6", 10 | "ofetch": "^1.3.4", 11 | "typescript": "^5.4.5" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /proxy/nitro.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineNitroConfig } from 'nitropack/config' 3 | 4 | export default defineNitroConfig({ 5 | routeRules: { 6 | '/**': { cors: true, swr: 3600 }, 7 | }, 8 | runtimeConfig: { 9 | tmdb: { 10 | apiKey: process.env.TMDB_API_KEY || '', 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineVitestConfig } from '@nuxt/test-utils/config' 2 | 3 | export default defineVitestConfig({ 4 | test: { 5 | include: ['**/tests/unit/**/*.test.ts', '**/components/**/*.test.ts', '**/composables/**/*.test.ts'], 6 | name: 'unit', 7 | environment: 'node', 8 | coverage: { 9 | include: ['**/components/**/*.vue', '**/composables/**/*.ts'], 10 | }, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /components/carousel/Items.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /tests/e2e/homapage.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@nuxt/test-utils/playwright' 2 | 3 | test('homepage displays correctly 1', async ({ page, goto }) => { 4 | await goto('', { waitUntil: 'hydration' }) 5 | await expect(page.getByRole('heading')).toContainText('', { ignoreCase: true }) 6 | }) 7 | 8 | test('homepage displays correctly 2', async ({ page, goto }) => { 9 | await goto('', { waitUntil: 'hydration' }) 10 | await expect(page.getByRole('heading')).toContainText('', { ignoreCase: true }) 11 | }) 12 | -------------------------------------------------------------------------------- /components/media/Videos.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | -------------------------------------------------------------------------------- /.github/workflows/provenance.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | permissions: 11 | contents: read 12 | jobs: 13 | check-provenance: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Check provenance downgrades 20 | uses: danielroe/provenance-action@a5a718233ca12eff67651fcf29a030bbbd5b3ca1 # v0.1.0 21 | with: 22 | fail-on-provenance-change: true 23 | -------------------------------------------------------------------------------- /components/media/Overview.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | -------------------------------------------------------------------------------- /pages/person/[id].vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /components/StarsRate.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 22 | -------------------------------------------------------------------------------- /proxy/routes/ipx/[...path].ts: -------------------------------------------------------------------------------- 1 | import { lazyEventHandler, useBase } from 'h3' 2 | import { createIPX, createIPXH3Handler, ipxHttpStorage } from 'ipx' 3 | 4 | export default lazyEventHandler(() => { 5 | const ipx = createIPX({ 6 | maxAge: 3600, 7 | alias: { 8 | '/tmdb': 'https://image.tmdb.org/t/p/original/', 9 | '/youtube': 'https://img.youtube.com/', 10 | }, 11 | storage: ipxHttpStorage({ 12 | domains: [ 13 | 'image.tmdb.org', 14 | 'img.youtube.com', 15 | ], 16 | }), 17 | }) 18 | 19 | return useBase('/ipx', createIPXH3Handler(ipx)) 20 | }) 21 | -------------------------------------------------------------------------------- /components/photo/Card.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /constants/lists.ts: -------------------------------------------------------------------------------- 1 | import type { QueryItem } from '~/types' 2 | 3 | export const QUERY_LIST = { 4 | movie: ([ 5 | { type: 'movie', title: 'Popular Movies', query: 'popular' }, 6 | { type: 'movie', title: 'Top Rated Movies', query: 'top_rated' }, 7 | { type: 'movie', title: 'Upcoming Movies', query: 'upcoming' }, 8 | { type: 'movie', title: 'Now Playing Movies', query: 'now_playing' }, 9 | ]), 10 | tv: ([ 11 | { type: 'tv', title: 'Popular TV Shows', query: 'popular' }, 12 | { type: 'tv', title: 'Top Rated TV Shows', query: 'top_rated' }, 13 | { type: 'tv', title: 'TV Shows Airing Today', query: 'airing_today' }, 14 | ]), 15 | } 16 | -------------------------------------------------------------------------------- /pages/[type]/category/[query].vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /pages/genre/[no]/tv.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /pages/genre/[no]/movie.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 27 | -------------------------------------------------------------------------------- /components/LanguageSwitcher.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /components/carousel/AutoQuery.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /components/person/Credits.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /composables/item.ts: -------------------------------------------------------------------------------- 1 | import type { Image, Media, Video } from '~/types' 2 | import { useSingleton } from './utils' 3 | 4 | export function getTrailer(item: Media) { 5 | const trailer = item.videos?.results?.find(video => video.type === 'Trailer') 6 | return getVideoLink(trailer) 7 | } 8 | 9 | export function getVideoLink(item?: Video) { 10 | if (!item?.key) 11 | return null 12 | return `https://www.youtube.com/embed/${item.key}?rel=0&showinfo=0&autoplay=0` 13 | } 14 | 15 | const [ 16 | provideIframeModal, 17 | useIframeModal, 18 | ] = useSingleton<(url: string) => void>() 19 | 20 | const [ 21 | provideImageModal, 22 | useImageModal, 23 | ] = useSingleton<(photos: Image[], index: number) => void>() 24 | 25 | export { 26 | provideIframeModal, 27 | provideImageModal, 28 | useIframeModal, 29 | useImageModal, 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/tests-unit.yml: -------------------------------------------------------------------------------- 1 | name: Tests unit 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | push: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: true 16 | 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | - run: corepack enable 20 | - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 21 | with: 22 | node-version: lts/-1 23 | cache: pnpm 24 | 25 | - name: Install dependencies 26 | run: pnpm install 27 | 28 | - name: Run tests 29 | run: pnpm run test:unit 30 | 31 | - name: Lint project 32 | run: pnpm run lint 33 | 34 | - name: Typecheck project 35 | run: pnpm run typecheck 36 | -------------------------------------------------------------------------------- /components/person/Photos.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /proxy/README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Movies Proxy 2 | 3 | Proxy hosts is a lightweight proxy server for the tmdb api and youtube images. 4 | 5 | - Speeds up API responses by leveraging the SWR cache. 6 | - Speeds up the development by removing the requirement of having a local token set up. 7 | - Speeds up the performances by optimizing images using [unjs/ipx](https://github.com/unjs/ipx). 8 | - Allows easily deploying the main project to any hosting platform, yet leveraging caching and image optimization. 9 | 10 | ## Setup 11 | 12 | 1. Take a copy of `.env.example` and re-name to `.env` 13 | 2. Get your [TMDB](https://developers.themoviedb.org/3) API key 14 | 3. Enter the details into the `.env` file 15 | 4. Start the dev server with the following scripts 16 | 17 | ``` bash 18 | # Enable pnpm 19 | $ corepack enable 20 | 21 | # Install dependencies 22 | $ pnpm install 23 | 24 | # Start dev server with hot reload at localhost:3001 25 | $ pnpm dev 26 | ``` 27 | -------------------------------------------------------------------------------- /components/person/Card.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 25 | -------------------------------------------------------------------------------- /components/IframeModal.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 |