├── public ├── hero.png ├── logo.png ├── hero-bg.png ├── no-movie.png ├── readme │ ├── hero.png │ ├── jsmpro.png │ └── videokit.png ├── search.svg └── star.svg ├── .env.example ├── src ├── main.jsx ├── components │ ├── Search.jsx │ ├── MovieCard.jsx │ └── Spinner.jsx ├── App.css ├── appwrite.js ├── assets │ └── react.svg ├── index.css └── App.jsx ├── vite.config.js ├── .gitignore ├── index.html ├── package.json ├── eslint.config.js └── README.md /public/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/hero.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/hero-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/hero-bg.png -------------------------------------------------------------------------------- /public/no-movie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/no-movie.png -------------------------------------------------------------------------------- /public/readme/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/readme/hero.png -------------------------------------------------------------------------------- /public/readme/jsmpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/readme/jsmpro.png -------------------------------------------------------------------------------- /public/readme/videokit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adrianhajdin/react-movies/HEAD/public/readme/videokit.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | VITE_TMDB_API_KEY= 2 | 3 | VITE_APPWRITE_PROJECT_ID= 4 | VITE_APPWRITE_DATABASE_ID= 5 | VITE_APPWRITE_COLLECTION_ID= -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | 6 | createRoot(document.getElementById('root')).render( 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tailwindcss()], 8 | }) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env.example 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TV Time 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Search.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Search = ({ searchTerm, setSearchTerm }) => { 4 | return ( 5 |
6 |
7 | search 8 | 9 | setSearchTerm(e.target.value)} 14 | /> 15 |
16 |
17 | ) 18 | } 19 | export default Search 20 | -------------------------------------------------------------------------------- /public/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-first-react-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tailwindcss/vite": "^4.0.0", 14 | "appwrite": "^18.1.1", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-use": "^17.6.0", 18 | "tailwindcss": "^4.0.0" 19 | }, 20 | "devDependencies": { 21 | "@eslint/js": "^9.17.0", 22 | "@types/react": "^18.3.18", 23 | "@types/react-dom": "^18.3.5", 24 | "@vitejs/plugin-react": "^4.3.4", 25 | "eslint": "^9.17.0", 26 | "eslint-plugin-react": "^7.37.2", 27 | "eslint-plugin-react-hooks": "^5.0.0", 28 | "eslint-plugin-react-refresh": "^0.4.16", 29 | "globals": "^15.14.0", 30 | "vite": "^6.0.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/MovieCard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const MovieCard = ({ movie: 4 | { title, vote_average, poster_path, release_date, original_language } 5 | }) => { 6 | return ( 7 |
8 | {title} 13 | 14 |
15 |

{title}

16 | 17 |
18 |
19 | Star Icon 20 |

{vote_average ? vote_average.toFixed(1) : 'N/A'}

21 |
22 | 23 | 24 |

{original_language}

25 | 26 | 27 |

28 | {release_date ? release_date.split('-')[0] : 'N/A'} 29 |

30 |
31 |
32 |
33 | ) 34 | } 35 | export default MovieCard 36 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /src/components/Spinner.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Spinner = () => { 4 | return ( 5 |
6 | 16 | Loading... 17 |
18 | ) 19 | } 20 | export default Spinner 21 | -------------------------------------------------------------------------------- /src/appwrite.js: -------------------------------------------------------------------------------- 1 | import { Client, Databases, ID, Query } from 'appwrite' 2 | 3 | const PROJECT_ID = import.meta.env.VITE_APPWRITE_PROJECT_ID; 4 | const DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID; 5 | const COLLECTION_ID = import.meta.env.VITE_APPWRITE_COLLECTION_ID; 6 | 7 | const client = new Client() 8 | .setEndpoint('https://fra.cloud.appwrite.io/v1') 9 | .setProject(PROJECT_ID) 10 | 11 | const database = new Databases(client); 12 | 13 | export const updateSearchCount = async (searchTerm, movie) => { 14 | // 1. Use Appwrite SDK to check if the search term exists in the database 15 | try { 16 | const result = await database.listDocuments(DATABASE_ID, COLLECTION_ID, [ 17 | Query.equal('searchTerm', searchTerm), 18 | ]) 19 | 20 | // 2. If it does, update the count 21 | if(result.documents.length > 0) { 22 | const doc = result.documents[0]; 23 | 24 | await database.updateDocument(DATABASE_ID, COLLECTION_ID, doc.$id, { 25 | count: doc.count + 1, 26 | }) 27 | // 3. If it doesn't, create a new document with the search term and count as 1 28 | } else { 29 | await database.createDocument(DATABASE_ID, COLLECTION_ID, ID.unique(), { 30 | searchTerm, 31 | count: 1, 32 | movie_id: movie.id, 33 | poster_url: `https://image.tmdb.org/t/p/w500${movie.poster_path}`, 34 | }) 35 | } 36 | } catch (error) { 37 | console.error(error); 38 | } 39 | } 40 | 41 | export const getTrendingMovies = async () => { 42 | try { 43 | const result = await database.listDocuments(DATABASE_ID, COLLECTION_ID, [ 44 | Query.limit(5), 45 | Query.orderDesc("count") 46 | ]) 47 | 48 | return result.documents; 49 | } catch (error) { 50 | console.error(error); 51 | } 52 | } -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap"); 3 | 4 | @import "tailwindcss"; 5 | 6 | @theme { 7 | --color-primary: #030014; 8 | 9 | --color-light-100: #cecefb; 10 | --color-light-200: #a8b5db; 11 | 12 | --color-gray-100: #9ca4ab; 13 | 14 | --color-dark-100: #0f0d23; 15 | 16 | --font-dm-sans: DM Sans, sans-serif; 17 | 18 | --breakpoint-xs: 480px; 19 | 20 | --background-image-hero-pattern: url("/hero-bg.png"); 21 | } 22 | 23 | @layer base { 24 | body { 25 | font-family: "DM Sans", serif; 26 | font-optical-sizing: auto; 27 | background: #030014; 28 | } 29 | 30 | h1 { 31 | @apply mx-auto max-w-4xl text-center text-5xl font-bold leading-tight tracking-[-1%] text-white sm:text-[64px] sm:leading-[76px]; 32 | } 33 | 34 | h2 { 35 | @apply text-2xl font-bold text-white sm:text-3xl; 36 | } 37 | 38 | main { 39 | @apply min-h-screen relative bg-primary; 40 | } 41 | 42 | header { 43 | @apply sm:mt-10 mt-5; 44 | } 45 | 46 | header img { 47 | @apply w-full max-w-lg h-auto object-contain mx-auto drop-shadow-md; 48 | } 49 | } 50 | 51 | @layer components { 52 | .pattern { 53 | @apply bg-hero-pattern w-full h-screen bg-center bg-cover absolute z-0; 54 | } 55 | 56 | .wrapper { 57 | @apply px-5 py-12 xs:p-10 max-w-7xl mx-auto flex flex-col relative z-10; 58 | } 59 | 60 | .trending { 61 | @apply mt-20; 62 | 63 | & ul { 64 | @apply flex flex-row overflow-y-auto gap-5 -mt-10 w-full hide-scrollbar; 65 | } 66 | 67 | & ul li { 68 | @apply min-w-[230px] flex flex-row items-center; 69 | } 70 | 71 | & ul li p { 72 | @apply fancy-text mt-[22px] text-nowrap; 73 | } 74 | 75 | & ul li img { 76 | @apply w-[127px] h-[163px] rounded-lg object-cover -ml-3.5; 77 | } 78 | } 79 | 80 | .search { 81 | @apply w-full bg-light-100/5 px-4 py-3 rounded-lg mt-10 max-w-3xl mx-auto; 82 | 83 | & div { 84 | @apply relative flex items-center; 85 | } 86 | 87 | & img { 88 | @apply absolute left-2 h-5 w-5; 89 | } 90 | 91 | & input { 92 | @apply w-full bg-transparent py-2 sm:pr-10 pl-10 text-base text-gray-200 placeholder-light-200 outline-hidden; 93 | } 94 | } 95 | 96 | .all-movies { 97 | @apply space-y-9; 98 | 99 | & ul { 100 | @apply grid grid-cols-1 gap-5 xs:grid-cols-2 md:grid-cols-3 lg:grid-cols-4; 101 | } 102 | } 103 | 104 | .movie-card { 105 | @apply bg-dark-100 p-5 rounded-2xl shadow-inner shadow-light-100/10; 106 | 107 | & img { 108 | @apply rounded-lg h-auto w-full; 109 | } 110 | 111 | & h3 { 112 | @apply text-white font-bold text-base line-clamp-1; 113 | } 114 | 115 | & .content { 116 | @apply mt-2 flex flex-row items-center flex-wrap gap-2; 117 | } 118 | 119 | & .rating { 120 | @apply flex flex-row items-center gap-1; 121 | } 122 | 123 | & .rating img { 124 | @apply size-4 object-contain; 125 | } 126 | 127 | & .rating p { 128 | @apply font-bold text-base text-white; 129 | } 130 | 131 | & .content span { 132 | @apply text-sm text-gray-100; 133 | } 134 | 135 | & .content .lang { 136 | @apply capitalize text-gray-100 font-medium text-base; 137 | } 138 | 139 | & .content .year { 140 | @apply text-gray-100 font-medium text-base; 141 | } 142 | } 143 | } 144 | 145 | @utility text-gradient { 146 | @apply bg-linear-to-r from-[#D6C7FF] to-[#AB8BFF] bg-clip-text text-transparent; 147 | } 148 | 149 | @utility fancy-text { 150 | -webkit-text-stroke: 5px rgba(206, 206, 251, 0.5); 151 | font-size: 190px; 152 | font-family: "Bebas Neue", sans-serif; 153 | } 154 | 155 | @utility hide-scrollbar { 156 | -ms-overflow-style: none; 157 | scrollbar-width: none; 158 | 159 | &::-webkit-scrollbar { 160 | display: none; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import Search from './components/Search.jsx' 3 | import Spinner from './components/Spinner.jsx' 4 | import MovieCard from './components/MovieCard.jsx' 5 | import { useDebounce } from 'react-use' 6 | import { getTrendingMovies, updateSearchCount } from './appwrite.js' 7 | 8 | const API_BASE_URL = 'https://api.themoviedb.org/3'; 9 | 10 | const API_KEY = import.meta.env.VITE_TMDB_API_KEY; 11 | 12 | const API_OPTIONS = { 13 | method: 'GET', 14 | headers: { 15 | accept: 'application/json', 16 | Authorization: `Bearer ${API_KEY}` 17 | } 18 | } 19 | 20 | const App = () => { 21 | const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('') 22 | const [searchTerm, setSearchTerm] = useState(''); 23 | 24 | const [movieList, setMovieList] = useState([]); 25 | const [errorMessage, setErrorMessage] = useState(''); 26 | const [isLoading, setIsLoading] = useState(false); 27 | 28 | const [trendingMovies, setTrendingMovies] = useState([]); 29 | 30 | // Debounce the search term to prevent making too many API requests 31 | // by waiting for the user to stop typing for 500ms 32 | useDebounce(() => setDebouncedSearchTerm(searchTerm), 500, [searchTerm]) 33 | 34 | const fetchMovies = async (query = '') => { 35 | setIsLoading(true); 36 | setErrorMessage(''); 37 | 38 | try { 39 | const endpoint = query 40 | ? `${API_BASE_URL}/search/movie?query=${encodeURIComponent(query)}` 41 | : `${API_BASE_URL}/discover/movie?sort_by=popularity.desc`; 42 | 43 | const response = await fetch(endpoint, API_OPTIONS); 44 | 45 | if(!response.ok) { 46 | throw new Error('Failed to fetch movies'); 47 | } 48 | 49 | const data = await response.json(); 50 | 51 | if(data.Response === 'False') { 52 | setErrorMessage(data.Error || 'Failed to fetch movies'); 53 | setMovieList([]); 54 | return; 55 | } 56 | 57 | setMovieList(data.results || []); 58 | 59 | if(query && data.results.length > 0) { 60 | await updateSearchCount(query, data.results[0]); 61 | } 62 | } catch (error) { 63 | console.error(`Error fetching movies: ${error}`); 64 | setErrorMessage('Error fetching movies. Please try again later.'); 65 | } finally { 66 | setIsLoading(false); 67 | } 68 | } 69 | 70 | const loadTrendingMovies = async () => { 71 | try { 72 | const movies = await getTrendingMovies(); 73 | 74 | setTrendingMovies(movies); 75 | } catch (error) { 76 | console.error(`Error fetching trending movies: ${error}`); 77 | } 78 | } 79 | 80 | useEffect(() => { 81 | fetchMovies(debouncedSearchTerm); 82 | }, [debouncedSearchTerm]); 83 | 84 | useEffect(() => { 85 | loadTrendingMovies(); 86 | }, []); 87 | 88 | return ( 89 |
90 |
91 | 92 |
93 |
94 | Hero Banner 95 |

Find Movies You'll Enjoy Without the Hassle

96 | 97 | 98 |
99 | 100 | {trendingMovies.length > 0 && ( 101 |
102 |

Trending Movies

103 | 104 |
    105 | {trendingMovies.map((movie, index) => ( 106 |
  • 107 |

    {index + 1}

    108 | {movie.title} 109 |
  • 110 | ))} 111 |
112 |
113 | )} 114 | 115 |
116 |

All Movies

117 | 118 | {isLoading ? ( 119 | 120 | ) : errorMessage ? ( 121 |

{errorMessage}

122 | ) : ( 123 |
    124 | {movieList.map((movie) => ( 125 | 126 | ))} 127 |
128 | )} 129 |
130 |
131 |
132 | ) 133 | } 134 | 135 | export default App 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Project Banner 5 | 6 |
7 | 8 |
9 | react.js 10 | appwrite 11 | tailwindcss 12 |
13 | 14 |

A Movie Application

15 | 16 |
17 | Build this project step by step with our detailed tutorial on JavaScript Mastery YouTube. Join the JSM family! 18 |
19 |
20 | 21 | ## 📋 Table of Contents 22 | 23 | 1. 🤖 [Introduction](#introduction) 24 | 2. ⚙️ [Tech Stack](#tech-stack) 25 | 3. 🔋 [Features](#features) 26 | 4. 🤸 [Quick Start](#quick-start) 27 | 5. 🕸️ [Snippets (Code to Copy)](#snippets) 28 | 6. 🔗 [Assets](#links) 29 | 7. 🚀 [More](#more) 30 | 31 | ## 🚨 Tutorial 32 | 33 | This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, JavaScript Mastery. 34 | 35 | If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner! 36 | 37 | 38 | 39 | ## 🤖 Introduction 40 | 41 | Built with React.js for the user interface, Appwrite for backend services, and styled with TailwindCSS, this Movie App lets users browse trending movies, search titles, and explore content using the TMDB API. It features a responsive layout and a sleek, modern design. 42 | 43 | If you're getting started and need assistance or face any bugs, join our active Discord community with over **50k+** members. It's a place where people help each other out. 44 | 45 | 46 | 47 | ## ⚙️ Tech Stack 48 | 49 | - **[Appwrite](https://appwrite.io/)** is an open-source Backend-as-a-Service (BaaS) platform that provides developers with a set of APIs to manage authentication, databases, storage, and more, enabling rapid development of secure and scalable applications. 50 | 51 | - **[React.js](https://react.dev/reference/react)** is a JavaScript library developed by Meta for building user interfaces. It allows developers to create reusable UI components that manage their own state, leading to more efficient and predictable code. React is widely used for developing single-page applications (SPAs) due to its virtual DOM that improves performance and ease of maintenance. 52 | 53 | - **[React-use](https://github.com/streamich/react-use)** is a collection of essential React hooks that simplify common tasks like managing state, side effects, and lifecycle events, promoting cleaner and more maintainable code in React applications. 54 | 55 | - **[Tailwind CSS](https://tailwindcss.com/)** is a utility-first CSS framework that provides low-level utility classes to build custom designs without writing custom CSS, enabling rapid and responsive UI development. 56 | 57 | - **[Vite](https://vite.dev/)** is a modern build tool that provides a fast development environment for frontend projects. It offers features like hot module replacement (HMR) and optimized builds, enhancing the development experience and performance. 58 | 59 | 60 | ## 🔋 Features 61 | 62 | 👉 **Browse All Movies**: Explore a wide range of movies available on the platform. 63 | 64 | 👉 **Search Movies**: Easily search for specific movies using a search function. 65 | 66 | 👉 **Trending Movies Algorithm**: Displays trending movies based on a dynamic algorithm. 67 | 68 | 👉 **Modern UI/UX**: A sleek and user-friendly interface designed for a great experience. 69 | 70 | 👉 **Responsiveness**: Fully responsive design that works seamlessly across devices. 71 | 72 | and many more, including code architecture and reusability 73 | 74 | ## 🤸 Quick Start 75 | 76 | Follow these steps to set up the project locally on your machine. 77 | 78 | **Prerequisites** 79 | 80 | Make sure you have the following installed on your machine: 81 | 82 | - [Git](https://git-scm.com/) 83 | - [Node.js](https://nodejs.org/en) 84 | - [npm](https://www.npmjs.com/) (Node Package Manager) 85 | 86 | **Cloning the Repository** 87 | 88 | ```bash 89 | git clone https://github.com/adrianhajdin/react-movies.git 90 | cd react-movies 91 | 92 | ``` 93 | 94 | **Installation** 95 | 96 | Install the project dependencies using npm: 97 | 98 | ```bash 99 | npm install 100 | ``` 101 | 102 | **Set Up Environment Variables** 103 | 104 | Create a new file named `.env.local` in the root of your project and add the following content: 105 | 106 | ```env 107 | VITE_TMDB_API_KEY= 108 | 109 | VITE_APPWRITE_PROJECT_ID= 110 | VITE_APPWRITE_DATABASE_ID= 111 | VITE_APPWRITE_COLLECTION_ID= 112 | ``` 113 | 114 | Replace the placeholder values with your actual **[TheMovieDatabase API](https://developer.themoviedb.org/reference/intro/getting-started)** and **[Appwrite](https://apwr.dev/JSM050)** credentials. 115 | 116 | **Running the Project** 117 | 118 | ```bash 119 | npm run dev 120 | ``` 121 | 122 | Open [http://localhost:5173](http://localhost:5173) in your browser to view the project. 123 | 124 | ## 🕸️ Snippets 125 | 126 |
127 | index.css 128 | 129 | ```css 130 | @import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap"); 131 | @import url("https://fonts.googleapis.com/css2?family=Bebas+Neue&display=swap"); 132 | 133 | @import "tailwindcss"; 134 | 135 | @theme { 136 | --color-primary: #030014; 137 | 138 | --color-light-100: #cecefb; 139 | --color-light-200: #a8b5db; 140 | 141 | --color-gray-100: #9ca4ab; 142 | 143 | --color-dark-100: #0f0d23; 144 | 145 | --font-dm-sans: DM Sans, sans-serif; 146 | 147 | --breakpoint-xs: 480px; 148 | 149 | --background-image-hero-pattern: url("/hero-bg.png"); 150 | } 151 | 152 | @layer base { 153 | body { 154 | font-family: "DM Sans", serif; 155 | font-optical-sizing: auto; 156 | background: #030014; 157 | } 158 | 159 | h1 { 160 | @apply mx-auto max-w-4xl text-center text-5xl font-bold leading-tight tracking-[-1%] text-white sm:text-[64px] sm:leading-[76px]; 161 | } 162 | 163 | h2 { 164 | @apply text-2xl font-bold text-white sm:text-3xl; 165 | } 166 | 167 | main { 168 | @apply min-h-screen relative bg-primary; 169 | } 170 | 171 | header { 172 | @apply sm:mt-10 mt-5; 173 | } 174 | 175 | header img { 176 | @apply w-full max-w-lg h-auto object-contain mx-auto drop-shadow-md; 177 | } 178 | } 179 | 180 | @layer components { 181 | .pattern { 182 | @apply bg-hero-pattern w-full h-screen bg-center bg-cover absolute z-0; 183 | } 184 | 185 | .wrapper { 186 | @apply px-5 py-12 xs:p-10 max-w-7xl mx-auto flex flex-col relative z-10; 187 | } 188 | 189 | .trending { 190 | @apply mt-20; 191 | 192 | & ul { 193 | @apply flex flex-row overflow-y-auto gap-5 -mt-10 w-full hide-scrollbar; 194 | } 195 | 196 | & ul li { 197 | @apply min-w-[230px] flex flex-row items-center; 198 | } 199 | 200 | & ul li p { 201 | @apply fancy-text mt-[22px] text-nowrap; 202 | } 203 | 204 | & ul li img { 205 | @apply w-[127px] h-[163px] rounded-lg object-cover -ml-3.5; 206 | } 207 | } 208 | 209 | .search { 210 | @apply w-full bg-light-100/5 px-4 py-3 rounded-lg mt-10 max-w-3xl mx-auto; 211 | 212 | & div { 213 | @apply relative flex items-center; 214 | } 215 | 216 | & img { 217 | @apply absolute left-2 h-5 w-5; 218 | } 219 | 220 | & input { 221 | @apply w-full bg-transparent py-2 sm:pr-10 pl-10 text-base text-gray-200 placeholder-light-200 outline-hidden; 222 | } 223 | } 224 | 225 | .all-movies { 226 | @apply space-y-9; 227 | 228 | & ul { 229 | @apply grid grid-cols-1 gap-5 xs:grid-cols-2 md:grid-cols-3 lg:grid-cols-4; 230 | } 231 | } 232 | 233 | .movie-card { 234 | @apply bg-dark-100 p-5 rounded-2xl shadow-inner shadow-light-100/10; 235 | 236 | & img { 237 | @apply rounded-lg h-auto w-full; 238 | } 239 | 240 | & h3 { 241 | @apply text-white font-bold text-base line-clamp-1; 242 | } 243 | 244 | & .content { 245 | @apply mt-2 flex flex-row items-center flex-wrap gap-2; 246 | } 247 | 248 | & .rating { 249 | @apply flex flex-row items-center gap-1; 250 | } 251 | 252 | & .rating img { 253 | @apply size-4 object-contain; 254 | } 255 | 256 | & .rating p { 257 | @apply font-bold text-base text-white; 258 | } 259 | 260 | & .content span { 261 | @apply text-sm text-gray-100; 262 | } 263 | 264 | & .content .lang { 265 | @apply capitalize text-gray-100 font-medium text-base; 266 | } 267 | 268 | & .content .year { 269 | @apply text-gray-100 font-medium text-base; 270 | } 271 | } 272 | } 273 | 274 | @utility text-gradient { 275 | @apply bg-linear-to-r from-[#D6C7FF] to-[#AB8BFF] bg-clip-text text-transparent; 276 | } 277 | 278 | @utility fancy-text { 279 | -webkit-text-stroke: 5px rgba(206, 206, 251, 0.5); 280 | font-size: 190px; 281 | font-family: "Bebas Neue", sans-serif; 282 | } 283 | 284 | @utility hide-scrollbar { 285 | -ms-overflow-style: none; 286 | scrollbar-width: none; 287 | 288 | &::-webkit-scrollbar { 289 | display: none; 290 | } 291 | } 292 | ``` 293 | 294 |
295 | 296 |
297 | components/Spinner.jsx 298 | 299 | ```jsx 300 | import React from 'react' 301 | 302 | const Spinner = () => { 303 | return ( 304 |
305 | 315 | Loading... 316 |
317 | ) 318 | } 319 | export default Spinner 320 | ``` 321 |
322 | 323 | 324 | ## 🔗 Assets 325 | 326 | Assets and snippets used in the project can be found in the **[video kit](https://jsm.dev/react25-kit)**. 327 | 328 | 329 | Video Kit Banner 330 | 331 | 332 | ## 🚀 More 333 | 334 | **Advance your skills with Next.js Pro Course** 335 | 336 | Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with 337 | detailed explanations, cool features, and exercises to boost your skills. Give it a go! 338 | 339 | 340 | Project Banner 341 | --------------------------------------------------------------------------------