├── .eslintrc.json ├── config └── index.js ├── public ├── roy.jpg ├── favicon.ico ├── vercel.svg ├── thirteen.svg └── next.svg ├── fonts ├── Mona-Sans.woff2 └── Hubot-Sans.woff2 ├── postcss.config.js ├── Requests.js ├── pages ├── 404.js ├── _document.tsx ├── api │ └── hello.ts ├── _app.tsx └── movie │ └── [id] │ └── index.js ├── .vscode └── settings.json ├── next.config.js ├── components ├── Footer.tsx ├── Avatar.tsx ├── Popover.tsx ├── PopularMovie.js ├── Meta.tsx ├── Scroll.js ├── MovieCard.js ├── AboutDialog.tsx ├── Row.js ├── Dialog.tsx ├── Nav.tsx ├── NavigationMenu.tsx └── Hero.tsx ├── app ├── blog │ └── page.tsx ├── Head.tsx ├── layout.tsx └── page.tsx ├── .gitignore ├── README.md ├── tailwind.config.js ├── tsconfig.json ├── axios.js ├── package.json └── styles └── globals.css /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | export const server = "https://api.themoviedb.org/3/movie"; 2 | -------------------------------------------------------------------------------- /public/roy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royquilor/movie-trailers/HEAD/public/roy.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royquilor/movie-trailers/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /fonts/Mona-Sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royquilor/movie-trailers/HEAD/fonts/Mona-Sans.woff2 -------------------------------------------------------------------------------- /fonts/Hubot-Sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/royquilor/movie-trailers/HEAD/fonts/Hubot-Sans.woff2 -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /Requests.js: -------------------------------------------------------------------------------- 1 | const requests = { 2 | fetchTrending: `/popular?api_key=${process.env.NEXT_PUBLIC_API_KEY}&language=en-US&page=1`, 3 | }; 4 | 5 | export default requests; 6 | -------------------------------------------------------------------------------- /pages/404.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotFound = () => { 4 | return
404 sorry dude
; 5 | }; 6 | 7 | export default NotFound; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "cSpell.words": ["modestbranding", "playsinline", "tmdb"] 5 | } 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | experimental: { 5 | appDir: true, 6 | }, 7 | images: { 8 | domains: ["image.tmdb.org"], 9 | }, 10 | }; 11 | 12 | module.exports = nextConfig; 13 | -------------------------------------------------------------------------------- /components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export const Footer = () => { 4 | return ( 5 | 9 | ); 10 | }; 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import Meta from "../../components/Meta"; 2 | 3 | export default function BlogPage() { 4 | return ( 5 |
6 | 11 |

Contact

12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /app/Head.tsx: -------------------------------------------------------------------------------- 1 | export default async function Head() { 2 | return ( 3 | <> 4 | Trending Movie Trailers 5 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import { ReactNode } from "react"; 3 | 4 | import localFont from "@next/font/local"; 5 | import Nav from "../components/Nav"; 6 | const myFont = localFont({ 7 | src: "../fonts/Mona-Sans.woff2", 8 | variable: "--font-mona-sans", 9 | }); 10 | 11 | export default function RootLayout({ children }: { children: ReactNode }) { 12 | return ( 13 | 14 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as Avatar from "@radix-ui/react-avatar"; 2 | 3 | const AvatarDemo = () => ( 4 | 5 | 10 | 11 | RQ 12 | 13 | 14 | ); 15 | 16 | export default AvatarDemo; 17 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Popover.tsx: -------------------------------------------------------------------------------- 1 | import * as Popover from "@radix-ui/react-popover"; 2 | import { ReactNode } from "react"; 3 | 4 | const PopoverDemo = ({ children }: { children: ReactNode }) => ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default PopoverDemo; 19 | -------------------------------------------------------------------------------- /components/PopularMovie.js: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import MovieCard from "./MovieCard"; 3 | import ScrollAreaDemo from "./Scroll"; 4 | 5 | const PopularMovie = ({ movies }) => { 6 | return ( 7 |
8 |

Popular

9 | 10 |
11 | {movies.map((movie) => ( 12 | 13 | ))} 14 |
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default PopularMovie; 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trending Movie Trailers 2 | 3 | https://user-images.githubusercontent.com/2366186/218274624-4e53cfd3-5197-4324-b578-469629bebf23.mp4 4 | 5 | A fun project to learn Next JS, Framer Motion and TMDB. 6 | The project is hacked together following tutorials on Youtube and likely not using best practise as I'm a beginner with JavaScript. 7 | 8 | ## Notes 9 | 10 | ### Youtube 11 | 12 | https://www.npmjs.com/package/react-youtube 13 | 14 | ### New app directory 15 | 16 | Lets migrate an existing app 17 | 18 | https://youtube.com/live/5pcKS_gi-0Q?feature=shares&t=1220 19 | 20 | Continue 21 | https://youtube.com/live/5pcKS_gi-0Q?feature=shares&t=1948 22 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Hero from "../components/Hero"; 4 | import Row from "../components/Row"; 5 | import requests from "../Requests"; 6 | import { motion } from "framer-motion"; 7 | import Nav from "../components/Nav"; 8 | 9 | export default function Home() { 10 | return ( 11 | 16 |
17 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/Meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | interface Props { 4 | title: string; 5 | description: string; 6 | keywords: string; 7 | } 8 | 9 | const Meta = ({ title, description, keywords }: Props) => { 10 | return ( 11 | 12 | Create Next App 13 | 14 | 15 | 16 | 17 | 18 | {title} 19 | 20 | ); 21 | }; 22 | 23 | export default Meta; 24 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { motion } from "framer-motion"; 4 | 5 | import localFont from "@next/font/local"; 6 | const myFont = localFont({ 7 | src: "../fonts/Mona-Sans.woff2", 8 | variable: "--font-mona-sans", 9 | }); 10 | 11 | export default function App({ Component, pageProps }: AppProps) { 12 | return ( 13 | 18 |
19 | 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | const { fontFamily } = require("tailwindcss/defaultTheme"); 3 | 4 | module.exports = { 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx}", 7 | "./components/**/*.{js,ts,jsx,tsx}", 8 | ], 9 | theme: { 10 | extend: { 11 | // fontFamily: { 12 | // sans: ["var(--font-mona-sans)", ...fontFamily.sans], 13 | // }, 14 | spacing: { 15 | "2/3": "100%", 16 | }, 17 | }, 18 | }, 19 | corePlugins: { 20 | aspectRatio: false, 21 | }, 22 | plugins: [ 23 | require("@tailwindcss/typography"), 24 | require("@tailwindcss/forms"), 25 | require("@tailwindcss/aspect-ratio"), 26 | require("@tailwindcss/line-clamp"), 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /components/Scroll.js: -------------------------------------------------------------------------------- 1 | import * as ScrollArea from "@radix-ui/react-scroll-area"; 2 | 3 | const ScrollAreaDemo = ({ children }) => ( 4 | 5 | 6 | {children} 7 | 8 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | export default ScrollAreaDemo; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ] 22 | }, 23 | "include": [ 24 | "next-env.d.ts", 25 | "**/*.ts", 26 | "**/*.tsx", 27 | ".next/types/**/*.ts", 28 | "components/Scroll.js", 29 | "components/Row.js", 30 | "components/PopularMovie.js", 31 | "components/Hero.tsx", 32 | "components/MovieCard.js", 33 | "app/[id]/index.js" 34 | ], 35 | "exclude": ["node_modules"] 36 | } 37 | -------------------------------------------------------------------------------- /components/MovieCard.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | import { motion } from "framer-motion"; 6 | 7 | const MovieCard = ({ movie }) => { 8 | return ( 9 | 13 |
14 | 19 | 26 | 27 |
28 | 29 | ); 30 | }; 31 | 32 | export default MovieCard; 33 | -------------------------------------------------------------------------------- /axios.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const instance = axios.create({ 4 | baseURL: "https://api.themoviedb.org/3/movie", 5 | }); 6 | 7 | // Request Interceptor 8 | instance.interceptors.request.use( 9 | config => { 10 | // You can modify the request config before sending it 11 | return config; 12 | }, 13 | error => { 14 | // Handle request errors 15 | console.error("Error during request:", error); 16 | return Promise.reject(error); 17 | } 18 | ); 19 | 20 | // Response Interceptor 21 | instance.interceptors.response.use( 22 | response => { 23 | // Any status code within the range of 2xx causes this function to trigger 24 | return response; 25 | }, 26 | error => { 27 | // Any status codes outside the range of 2xx cause this function to trigger 28 | console.error("Error during response:", error.response); 29 | // You can handle the error or pass it on to be handled by the calling function 30 | return Promise.reject(error); 31 | } 32 | ); 33 | 34 | export default instance; 35 | -------------------------------------------------------------------------------- /components/AboutDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Dialog from "@radix-ui/react-dialog"; 4 | import { ReactNode } from "react"; 5 | 6 | const AboutDialog = ({ 7 | text, 8 | children, 9 | }: { 10 | text: string; 11 | children: ReactNode; 12 | }) => ( 13 | 14 | 15 | {text} 16 | 17 | 18 | 19 | 20 | {children} 21 | About 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | 29 | export default AboutDialog; 30 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Row.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import axios from "../axios"; 3 | import React, { useEffect, useState } from "react"; 4 | import MovieCard from "./MovieCard"; 5 | import ScrollAreaDemo from "./Scroll"; 6 | 7 | function Row({ title, fetchUrl }) { 8 | const [movies, setMovies] = React.useState([]); 9 | // const [hasMounted, setHasMounted] = useState(false); 10 | useEffect(() => { 11 | async function fetchData() { 12 | const request = await axios.get(fetchUrl); 13 | setMovies(request.data.results); 14 | 15 | return request; 16 | } 17 | 18 | fetchData(); 19 | }, [fetchUrl]); 20 | 21 | // console.log(movies); 22 | if (!movies) return null; 23 | 24 | return ( 25 |
26 |

{title}

27 | 28 |
29 | {movies.map((movie) => ( 30 | 31 | ))} 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | export default Row; 39 | -------------------------------------------------------------------------------- /components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Dialog from "@radix-ui/react-dialog"; 4 | import { ReactNode } from "react"; 5 | 6 | type DialogDemoProps = { 7 | children: ReactNode; 8 | onClose?: () => void; // Adding an optional onClose prop 9 | }; 10 | 11 | const DialogDemo = ({ children, onClose }: DialogDemoProps ) => ( 12 | 13 | 14 | Play 15 | 16 | 17 | 18 | 19 | {children} 20 | Tweet 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export default DialogDemo; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "design-system", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@next/font": "13.1.1", 13 | "@radix-ui/react-avatar": "^1.0.1", 14 | "@radix-ui/react-dialog": "^1.0.2", 15 | "@radix-ui/react-navigation-menu": "^1.1.1", 16 | "@radix-ui/react-popover": "^1.0.3", 17 | "@radix-ui/react-scroll-area": "^1.0.2", 18 | "@types/node": "18.11.18", 19 | "@types/react": "18.0.26", 20 | "@types/react-dom": "18.0.10", 21 | "axios": "^1.2.3", 22 | "classnames": "^2.3.2", 23 | "eslint": "8.31.0", 24 | "eslint-config-next": "13.1.1", 25 | "framer-motion": "^8.5.2", 26 | "next": "13.1.1", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "react-youtube": "^10.1.0", 30 | "typescript": "4.9.4" 31 | }, 32 | "devDependencies": { 33 | "@tailwindcss/aspect-ratio": "^0.4.2", 34 | "@tailwindcss/forms": "^0.5.3", 35 | "@tailwindcss/line-clamp": "^0.4.2", 36 | "@tailwindcss/typography": "^0.5.8", 37 | "autoprefixer": "^10.4.13", 38 | "postcss": "^8.4.21", 39 | "tailwindcss": "^3.2.4" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import AvatarDemo from "../components/Avatar"; 2 | import { Dialog } from "@radix-ui/react-dialog"; 3 | import Link from "next/link"; 4 | import AboutDialog from "./AboutDialog"; 5 | import NavigationMenuDemo from "./NavigationMenu"; 6 | 7 | const Nav = () => { 8 | return ( 9 |
10 |
11 | Trailers 12 | 13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 |

21 | Trending movie trailers 22 |

23 |

24 | Passion project by Roy Quilor. Built with Next JS, Tailwind CSS, 25 | Radix UI, TMDB and Vercel. 26 |

27 |
28 |
29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Nav; 36 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/NavigationMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as NavigationMenu from "@radix-ui/react-navigation-menu"; 3 | import classNames from "classnames"; 4 | 5 | const NavigationMenuDemo = () => { 6 | return ( 7 | 8 | 9 | 10 | 11 | Learn 12 | 13 | 14 | Yes 15 | 16 | 17 | 18 | 19 | 20 | Overview 21 | 22 | 23 | Hello 24 | 25 | 26 | 27 | 28 | 29 | Contact 30 | 31 | 32 | Say hello 33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 | ); 46 | }; 47 | 48 | // const ListItem = React.forwardRef( 49 | // ({ className, children, title, ...props }, forwardedRef) => ( 50 | //
  • 51 | // 52 | // 57 | //
    {title}
    58 | //

    {children}

    59 | //
    60 | //
    61 | //
  • 62 | // ) 63 | // ); 64 | 65 | export default NavigationMenuDemo; 66 | -------------------------------------------------------------------------------- /components/Hero.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import axios from "../axios"; 3 | import requests from "../Requests"; 4 | import Image from "next/image"; 5 | import YouTube from "react-youtube"; 6 | import DialogDemo from "./Dialog"; // Import the dialog component 7 | 8 | // Define a type for the movie object 9 | type Movie = { 10 | id: number; 11 | title?: string; 12 | name?: string; 13 | original_name?: string; 14 | backdrop_path?: string; 15 | overview?: string; 16 | videos?: { 17 | results: Array<{ 18 | key: string; 19 | type: string; 20 | }>; 21 | }; 22 | }; 23 | 24 | function Hero() { 25 | const [heroMovie, setMovie] = useState(null); 26 | const [showTrailer, setShowTrailer] = useState(false); // State to control trailer dialog visibility 27 | 28 | const opts = { 29 | height: "390", 30 | width: "640", 31 | playerVars: { 32 | // https://developers.google.com/youtube/player_parameters 33 | autoplay: 1, 34 | rel: 0, 35 | modestbranding: 1, 36 | playsinline: 0, 37 | iv_load_policy: 3, 38 | }, 39 | }; 40 | 41 | useEffect(() => { 42 | async function fetchData() { 43 | const request = await axios.get(requests.fetchTrending); 44 | const randomIndex = Math.floor(Math.random() * request.data.results.length); 45 | const randomMovie = request.data.results[randomIndex]; 46 | // Fetch additional data for the trailer 47 | const movieDetails = await axios.get(`${randomMovie.id}?api_key=${process.env.NEXT_PUBLIC_API_KEY}&append_to_response=videos`); 48 | setMovie(movieDetails.data); 49 | } 50 | fetchData(); 51 | }, []); 52 | 53 | // Function to find the trailer 54 | const getTrailer = () => { 55 | return heroMovie?.videos?.results.find((vid) => vid.type.includes("Trailer")); 56 | }; 57 | 58 | return ( 59 |
    60 |
    61 |
    62 | {heroMovie?.backdrop_path && ( 63 | {heroMovie.title 70 | )} 71 |
    72 |
    73 |
    74 |
    75 |
    76 |

    77 | {heroMovie?.title || heroMovie?.name || heroMovie?.original_name} 78 |

    79 |
    80 | {heroMovie?.overview} 81 |
    82 | setShowTrailer(false)}> 83 | {heroMovie && getTrailer() && ( 84 | 89 | )} 90 | 91 | 92 |
    93 |
    94 | ); 95 | } 96 | 97 | export default Hero; 98 | -------------------------------------------------------------------------------- /pages/movie/[id]/index.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axios from "axios"; 4 | import Image from "next/image"; 5 | import React from "react"; 6 | import { server } from "../../../config"; 7 | import Meta from "../../../components/Meta"; 8 | import YouTube from "react-youtube"; 9 | import DialogDemo from "../../../components/Dialog"; 10 | import Link from "next/link"; 11 | 12 | const Movie = ({ movie }) => { 13 | const trailer = movie.videos.results.find((vid) => 14 | vid.type.includes("Trailer") 15 | ); 16 | const opts = { 17 | height: "390", 18 | width: "640", 19 | playerVars: { 20 | // https://developers.google.com/youtube/player_parameters 21 | autoplay: 1, 22 | rel: 0, 23 | modestbranding: 1, 24 | playsinline: 0, 25 | iv_load_policy: 3, 26 | }, 27 | }; 28 | return ( 29 |
    30 | 31 | 35 | 41 | 46 | 47 | 48 |
    49 | {movie.backdrop_path && ( 50 | {movie.title} 58 | )} 59 |
    60 | 61 |
    62 |
    63 | 64 | 69 | 70 | 71 |
    72 |
    73 |

    {movie.title}

    74 |
    75 |
    76 | {movie.overview} 77 |
    78 |
    79 |
    80 |
    Release
    81 |
    {movie.release_date}
    82 |
    83 |
    84 |
    Runtime
    85 |
    {movie.runtime} mins
    86 |
    87 |
    88 |
    Genres
    89 |
    90 | {movie.genres.map((genre) => genre.name).join(", ")} 91 |
    92 |
    93 |
    94 |
    95 |
    96 |
    97 |
    98 | ); 99 | }; 100 | 101 | export async function getStaticProps(context) { 102 | const { id } = context.params; 103 | const res = await axios( 104 | `${server}/${id}?api_key=${process.env.NEXT_PUBLIC_API_KEY}&append_to_response=videos` 105 | ); 106 | const movie = res.data; 107 | return { 108 | props: { movie }, 109 | }; 110 | } 111 | 112 | export async function getStaticPaths() { 113 | const res = await axios( 114 | `${server}/popular?api_key=${process.env.NEXT_PUBLIC_API_KEY}&language=en-US&page=1` 115 | ); 116 | const movies = res.data.results; 117 | 118 | const ids = movies.map((movie) => movie.id); 119 | const paths = ids.map((id) => ({ params: { id: id.toString() } })); 120 | // console.log(movies); 121 | return { 122 | paths, 123 | fallback: false, 124 | }; 125 | } 126 | 127 | export default Movie; 128 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | @apply antialiased bg-black; 7 | } 8 | 9 | .ScrollAreaRoot { 10 | width: 100%; 11 | height: 100%; 12 | border-radius: 4px; 13 | overflow: hidden; 14 | --scrollbar-size: 10px; 15 | } 16 | 17 | .ScrollAreaViewport { 18 | width: 100%; 19 | height: 100%; 20 | border-radius: inherit; 21 | } 22 | 23 | .ScrollAreaScrollbar { 24 | display: flex; 25 | /* ensures no selection */ 26 | user-select: none; 27 | /* disable browser handling of all panning and zooming gestures on touch devices */ 28 | touch-action: none; 29 | padding: 2px; 30 | background: transparent; 31 | transition: background 160ms ease-out; 32 | } 33 | .ScrollAreaScrollbar:hover { 34 | background: black; 35 | } 36 | .ScrollAreaScrollbar[data-orientation="vertical"] { 37 | width: var(--scrollbar-size); 38 | } 39 | .ScrollAreaScrollbar[data-orientation="horizontal"] { 40 | flex-direction: column; 41 | height: var(--scrollbar-size); 42 | } 43 | 44 | .ScrollAreaThumb { 45 | flex: 1; 46 | background: white; 47 | border-radius: var(--scrollbar-size); 48 | position: relative; 49 | } 50 | /* increase target size for touch devices https://www.w3.org/WAI/WCAG21/Understanding/target-size.html */ 51 | .ScrollAreaThumb::before { 52 | content: ""; 53 | position: absolute; 54 | top: 50%; 55 | left: 50%; 56 | transform: translate(-50%, -50%); 57 | width: 100%; 58 | height: 100%; 59 | min-width: 44px; 60 | min-height: 44px; 61 | } 62 | 63 | .ScrollAreaCorner { 64 | @apply bg-black; 65 | } 66 | 67 | .DialogOverlay { 68 | animation: overlayShow 300ms cubic-bezier(0.16, 1, 0.3, 1); 69 | } 70 | 71 | .DialogContent { 72 | animation: contentShow 300ms cubic-bezier(0.16, 1, 0.3, 1); 73 | } 74 | 75 | @keyframes overlayShow { 76 | from { 77 | opacity: 0; 78 | } 79 | to { 80 | opacity: 1; 81 | } 82 | } 83 | 84 | @keyframes contentShow { 85 | from { 86 | opacity: 0; 87 | transform: translate(-50%, -48%) scale(0.8); 88 | } 89 | to { 90 | opacity: 1; 91 | transform: translate(-50%, -50%) scale(1); 92 | } 93 | } 94 | 95 | .NavigationMenuRoot { 96 | position: relative; 97 | display: flex; 98 | justify-content: center; 99 | xwidth: 100vw; 100 | z-index: 9999; 101 | } 102 | 103 | .NavigationMenuList { 104 | display: flex; 105 | justify-content: center; 106 | background-color: white; 107 | padding: 4px; 108 | border-radius: 6px; 109 | list-style: none; 110 | box-shadow: 0 2px 10px black; 111 | margin: 0; 112 | } 113 | 114 | .NavigationMenuTrigger, 115 | .NavigationMenuLink { 116 | padding: 8px 12px; 117 | outline: none; 118 | user-select: none; 119 | font-weight: 500; 120 | line-height: 1; 121 | border-radius: 4px; 122 | font-size: 15px; 123 | color: purple; 124 | } 125 | .NavigationMenuTrigger:focus, 126 | .NavigationMenuLink:focus { 127 | box-shadow: 0 0 0 2px blue; 128 | } 129 | .NavigationMenuTrigger:hover, 130 | .NavigationMenuLink:hover { 131 | background-color: red; 132 | } 133 | 134 | .NavigationMenuTrigger { 135 | display: flex; 136 | align-items: center; 137 | justify-content: space-between; 138 | gap: 2px; 139 | } 140 | 141 | .NavigationMenuLink { 142 | display: block; 143 | text-decoration: none; 144 | font-size: 15px; 145 | line-height: 1; 146 | } 147 | 148 | .NavigationMenuContent { 149 | position: absolute; 150 | top: 0; 151 | left: 0; 152 | width: 100%; 153 | animation-duration: 250ms; 154 | animation-timing-function: ease; 155 | } 156 | .NavigationMenuContent[data-motion="from-start"] { 157 | animation-name: enterFromLeft; 158 | } 159 | .NavigationMenuContent[data-motion="from-end"] { 160 | animation-name: enterFromRight; 161 | } 162 | .NavigationMenuContent[data-motion="to-start"] { 163 | animation-name: exitToLeft; 164 | } 165 | .NavigationMenuContent[data-motion="to-end"] { 166 | animation-name: exitToRight; 167 | } 168 | @media only screen and (min-width: 600px) { 169 | .NavigationMenuContent { 170 | width: auto; 171 | } 172 | } 173 | 174 | .NavigationMenuIndicator { 175 | display: flex; 176 | align-items: flex-end; 177 | justify-content: center; 178 | height: 10px; 179 | top: 100%; 180 | overflow: hidden; 181 | z-index: 1; 182 | transition: width, transform 250ms ease; 183 | } 184 | .NavigationMenuIndicator[data-state="visible"] { 185 | animation: fadeIn 200ms ease; 186 | } 187 | .NavigationMenuIndicator[data-state="hidden"] { 188 | animation: fadeOut 200ms ease; 189 | } 190 | 191 | .NavigationMenuViewport { 192 | position: relative; 193 | transform-origin: top center; 194 | margin-top: 10px; 195 | width: 100%; 196 | background-color: white; 197 | border-radius: 6px; 198 | overflow: hidden; 199 | box-shadow: hsl(206 22% 7% / 35%) 0px 10px 38px -10px, 200 | hsl(206 22% 7% / 20%) 0px 10px 20px -15px; 201 | height: var(--radix-navigation-menu-viewport-height); 202 | transition: width, height, 300ms ease; 203 | } 204 | .NavigationMenuViewport[data-state="open"] { 205 | animation: scaleIn 200ms ease; 206 | } 207 | .NavigationMenuViewport[data-state="closed"] { 208 | animation: scaleOut 200ms ease; 209 | } 210 | @media only screen and (min-width: 600px) { 211 | .NavigationMenuViewport { 212 | width: var(--radix-navigation-menu-viewport-width); 213 | } 214 | } 215 | 216 | .ViewportPosition { 217 | position: absolute; 218 | display: flex; 219 | justify-content: center; 220 | width: 100%; 221 | top: 100%; 222 | left: 0; 223 | perspective: 2000px; 224 | } 225 | 226 | @keyframes enterFromRight { 227 | from { 228 | opacity: 0; 229 | transform: translateX(200px); 230 | } 231 | to { 232 | opacity: 1; 233 | transform: translateX(0); 234 | } 235 | } 236 | 237 | @keyframes enterFromLeft { 238 | from { 239 | opacity: 0; 240 | transform: translateX(-200px); 241 | } 242 | to { 243 | opacity: 1; 244 | transform: translateX(0); 245 | } 246 | } 247 | 248 | @keyframes exitToRight { 249 | from { 250 | opacity: 1; 251 | transform: translateX(0); 252 | } 253 | to { 254 | opacity: 0; 255 | transform: translateX(200px); 256 | } 257 | } 258 | 259 | @keyframes exitToLeft { 260 | from { 261 | opacity: 1; 262 | transform: translateX(0); 263 | } 264 | to { 265 | opacity: 0; 266 | transform: translateX(-200px); 267 | } 268 | } 269 | 270 | @keyframes scaleIn { 271 | from { 272 | opacity: 0; 273 | transform: rotateX(-30deg) scale(0.9); 274 | } 275 | to { 276 | opacity: 1; 277 | transform: rotateX(0deg) scale(1); 278 | } 279 | } 280 | 281 | @keyframes scaleOut { 282 | from { 283 | opacity: 1; 284 | transform: rotateX(0deg) scale(1); 285 | } 286 | to { 287 | opacity: 0; 288 | transform: rotateX(-10deg) scale(0.95); 289 | } 290 | } 291 | 292 | @keyframes fadeIn { 293 | from { 294 | opacity: 0; 295 | } 296 | to { 297 | opacity: 1; 298 | } 299 | } 300 | 301 | @keyframes fadeOut { 302 | from { 303 | opacity: 1; 304 | } 305 | to { 306 | opacity: 0; 307 | } 308 | } 309 | --------------------------------------------------------------------------------