├── .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 |
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 |
18 |
19 |
20 |
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 |
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 |
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 |
47 |
48 |
49 | {movie.backdrop_path && (
50 |
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 |
--------------------------------------------------------------------------------