├── .env.example ├── app ├── styles │ ├── main.css │ └── app.css ├── assets │ └── logo.png ├── entry.client.tsx ├── components │ ├── layout │ │ ├── main.tsx │ │ ├── sidebar.tsx │ │ ├── menu.tsx │ │ ├── menuItem.tsx │ │ ├── footer.tsx │ │ └── header.tsx │ ├── shared │ │ ├── mainTitle.tsx │ │ ├── error.tsx │ │ └── similarMovies.tsx │ ├── movies │ │ ├── movie │ │ │ ├── movieDetailButtons.tsx │ │ │ ├── movieDetailItem.tsx │ │ │ ├── movieDetailsImages.tsx │ │ │ └── movieDetailHeader.tsx │ │ ├── moviesDetailModal.tsx │ │ └── moviesCard.tsx │ ├── index.ts │ └── sections │ │ ├── heroSection.tsx │ │ └── categorySection.tsx ├── types │ ├── root.ts │ ├── videos.ts │ └── movies.ts ├── routes │ ├── movie │ │ ├── index.tsx │ │ └── $movieId.tsx │ ├── categories │ │ ├── index.tsx │ │ └── $categorySlug.tsx │ ├── index.tsx │ └── search │ │ └── $searchQuery.tsx ├── entry.server.tsx ├── helpers │ └── utils.ts ├── services │ └── apiService.ts └── root.tsx ├── .eslintrc.js ├── public └── favicon.ico ├── remix.env.d.ts ├── vercel.json ├── postcss.config.js ├── .gitignore ├── server.js ├── tsconfig.json ├── remix.config.js ├── tailwind.config.js ├── README.md └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | API_URL= 2 | API_KEY= 3 | -------------------------------------------------------------------------------- /app/styles/main.css: -------------------------------------------------------------------------------- 1 | #youtube div { 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@remix-run/eslint-config"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harundogdu/movie-app-with-remix/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harundogdu/movie-app-with-remix/HEAD/app/assets/logo.png -------------------------------------------------------------------------------- /remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "env": { 4 | "ENABLE_FILE_SYSTEM_API": "1" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | .cache 4 | .env 5 | .vercel 6 | .output 7 | 8 | /build/ 9 | /public/build 10 | /api/index.js 11 | /.idea -------------------------------------------------------------------------------- /app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import { RemixBrowser } from "remix"; 3 | 4 | ReactDOM.hydrate( 5 | , 6 | document 7 | ) -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import { createRequestHandler } from "@remix-run/vercel"; 2 | import * as build from "@remix-run/dev/server-build"; 3 | 4 | export default createRequestHandler({ build, mode: process.env.NODE_ENV }); 5 | -------------------------------------------------------------------------------- /app/components/layout/main.tsx: -------------------------------------------------------------------------------- 1 | import { ILayoutProps } from "~/types/root"; 2 | 3 | const Main = ({ children }: ILayoutProps) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Main; 8 | -------------------------------------------------------------------------------- /app/components/shared/mainTitle.tsx: -------------------------------------------------------------------------------- 1 | function MainTitle({ title }: { title: string }) { 2 | return ( 3 |

{title}

4 | ); 5 | } 6 | 7 | export default MainTitle; 8 | -------------------------------------------------------------------------------- /app/types/root.ts: -------------------------------------------------------------------------------- 1 | export interface IDocumentProps { 2 | children?: any; 3 | title?: string; 4 | } 5 | 6 | export interface ILayoutProps { 7 | children?: any; 8 | } 9 | 10 | export interface IErrorProps { 11 | error: Error 12 | } -------------------------------------------------------------------------------- /app/routes/movie/index.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunction, redirect } from "remix"; 2 | 3 | export const loader: LoaderFunction = async ({ params }) => { 4 | return redirect("/"); 5 | }; 6 | 7 | function Movies() { 8 | return
Error!
; 9 | } 10 | 11 | export default Movies; 12 | -------------------------------------------------------------------------------- /app/routes/categories/index.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunction, redirect } from "remix"; 2 | 3 | export const loader: LoaderFunction = async ({ params }) => { 4 | return redirect("/"); 5 | }; 6 | 7 | function Categories() { 8 | return
Error!
; 9 | } 10 | 11 | export default Categories; 12 | -------------------------------------------------------------------------------- /app/types/videos.ts: -------------------------------------------------------------------------------- 1 | export interface Result { 2 | iso_639_1: string; 3 | iso_3166_1: string; 4 | name: string; 5 | key: string; 6 | site: string; 7 | size: number; 8 | type: string; 9 | official: boolean; 10 | published_at: Date; 11 | id: string; 12 | } 13 | 14 | export interface IVideoProps { 15 | id: number; 16 | results: Result[]; 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "lib": ["DOM", "DOM.Iterable", "ES2019"], 5 | "isolatedModules": true, 6 | "esModuleInterop": true, 7 | "jsx": "react-jsx", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "target": "ES2019", 11 | "strict": true, 12 | "baseUrl": ".", 13 | "paths": { 14 | "~/*": ["./app/*"] 15 | }, 16 | 17 | // Remix takes care of building everything in `remix build`. 18 | "noEmit": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/components/shared/error.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "remix"; 2 | import { IErrorProps } from "~/types/root"; 3 | 4 | const Error = ({ error }: IErrorProps) => { 5 | return ( 6 |
7 |

{error.name}

8 |

Something went wrong!

9 | 10 | Go back to home 11 | 12 |
13 | ); 14 | }; 15 | 16 | export default Error; -------------------------------------------------------------------------------- /remix.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('@remix-run/dev').AppConfig} 3 | */ 4 | module.exports = { 5 | serverBuildTarget: "vercel", 6 | // When running locally in development mode, we use the built in remix 7 | // server. This does not understand the vercel lambda module format, 8 | // so we default back to the standard build output. 9 | server: process.env.NODE_ENV === "development" ? undefined : "./server.js", 10 | ignoredRouteFiles: [".*"], 11 | appDirectory: "app", 12 | assetsBuildDirectory: "public/build", 13 | serverBuildPath: "api/index.js", 14 | publicPath: "/build/", 15 | devServerPort: 8002 16 | }; 17 | -------------------------------------------------------------------------------- /app/entry.server.tsx: -------------------------------------------------------------------------------- 1 | import { renderToString } from "react-dom/server"; 2 | import { RemixServer } from "remix"; 3 | import type { EntryContext } from "remix"; 4 | 5 | export default function handleRequest( 6 | request: Request, 7 | responseStatusCode: number, 8 | responseHeaders: Headers, 9 | remixContext: EntryContext 10 | ) { 11 | const markup = renderToString( 12 | 13 | ); 14 | 15 | responseHeaders.set("Content-Type", "text/html"); 16 | 17 | return new Response("" + markup, { 18 | status: responseStatusCode, 19 | headers: responseHeaders, 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /app/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | export const customModalStyles = { 2 | content: { 3 | top: "50%", 4 | left: "80%", 5 | right: "auto", 6 | bottom: "auto", 7 | marginRight: "-50%", 8 | transform: "translate(-80%, -50%)", 9 | backgroundColor: "#000", 10 | color: "#fff", 11 | borderRadius: "4px", 12 | width: "80%", 13 | height: "70%", 14 | minWidth: "55%", 15 | minHeight: "50%", 16 | padding: "0", 17 | overflow: "hidden", 18 | border: "none", 19 | boxShadow: "0 0 20px rgba(0,0,0,0.5)", 20 | outline: "none", 21 | zIndex: "9999", 22 | }, 23 | }; -------------------------------------------------------------------------------- /app/components/movies/movie/movieDetailButtons.tsx: -------------------------------------------------------------------------------- 1 | import { HiExternalLink } from "react-icons/hi"; 2 | 3 | interface IMovieDetailButtonsProps { 4 | title: string; 5 | path: string; 6 | } 7 | 8 | export default function MovieDetailButtons({ title, path }: IMovieDetailButtonsProps) { 9 | return ( 10 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, useLoaderData } from "remix"; 2 | import { getNowPlayingMovies } from "~/services/apiService"; 3 | import { IMoviesProps } from "~/types/movies"; 4 | import { CategorySection } from "~/components"; 5 | 6 | export const loader: LoaderFunction = async () => { 7 | const { results } = await getNowPlayingMovies(); 8 | return json(results); 9 | }; 10 | 11 | export default function Index() { 12 | const movies = useLoaderData(); 13 | const heroSectionMovie = movies.find((movie) => movie.vote_average > 8) || movies[0]; 14 | return ( 15 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: 'class', 3 | content: [ 4 | "./app/**/*.{js,ts,jsx,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | colors: { 9 | primary: "#00112c", 10 | brand: "#050f2c", 11 | secondary: "#030b28", 12 | light: "#F0F0F0", 13 | brandYellow: "#F1D00A", 14 | detailPrimary: "rgba(61.5, 10.5, 10.5, .5)", 15 | detailSecondary: "rgba(71.5, 10.5, 10.5, 0.34)", 16 | 17 | }, 18 | variants: { 19 | scrollbar: ['dark'] 20 | }, 21 | }, 22 | 23 | }, 24 | plugins: [ 25 | require('tailwind-scrollbar'), 26 | ], 27 | } 28 | -------------------------------------------------------------------------------- /app/components/shared/similarMovies.tsx: -------------------------------------------------------------------------------- 1 | import { IMoviesProps } from "~/types/movies"; 2 | import { MainTitle, MoviesCard } from "~/components"; 3 | 4 | interface ISimilarMoviesProps { 5 | similarMovies: IMoviesProps[]; 6 | } 7 | 8 | function SimilarMovies({ similarMovies }: ISimilarMoviesProps) { 9 | return ( 10 |
11 | 12 |
13 | {similarMovies.map((movie, index) => { 14 | return ( 15 | 16 | ) 17 | })} 18 |
19 |
20 | ); 21 | } 22 | 23 | export default SimilarMovies; 24 | -------------------------------------------------------------------------------- /app/components/layout/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import Logo from "~/assets/logo.png"; 2 | import { NavLink } from "remix"; 3 | import { Menu } from "~/components"; 4 | 5 | function Sidebar() { 6 | return ( 7 | 16 | ); 17 | } 18 | 19 | export default Sidebar; 20 | -------------------------------------------------------------------------------- /app/components/movies/moviesDetailModal.tsx: -------------------------------------------------------------------------------- 1 | import Modal from "react-modal"; 2 | import YouTube from "react-youtube"; 3 | 4 | interface IMoviesDetailModalProps { 5 | modalIsOpen: boolean; 6 | setModalIsOpen: (isOpen: boolean) => void; 7 | customStyles: any; 8 | currentVideo: { 9 | key: string; 10 | }; 11 | } 12 | 13 | export default function MoviesDetailModal({ 14 | modalIsOpen, 15 | setModalIsOpen, 16 | currentVideo, 17 | customStyles 18 | }: IMoviesDetailModalProps) { 19 | return ( 20 | setModalIsOpen(false)} 23 | style={customStyles} 24 | > 25 |
26 | 31 |
32 |
33 | ); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /app/components/layout/menu.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineStar, AiOutlineStock } from "react-icons/ai"; 2 | import { BiMovie } from "react-icons/bi"; 3 | import { CgRowLast } from "react-icons/cg"; 4 | import { MenuItem } from "~/components"; 5 | 6 | function Menu() { 7 | const activeLinks = [ 8 | { 9 | id: 1, 10 | title: "Now Playing", 11 | path: "/", 12 | icon: , 13 | }, 14 | { 15 | id: 2, 16 | title: "Popular", 17 | path: "/categories/popular", 18 | icon: , 19 | }, 20 | { 21 | id: 3, 22 | title: "Top Rated", 23 | path: "/categories/top-rated", 24 | icon: , 25 | }, 26 | { 27 | id: 4, 28 | title: "Upcoming", 29 | path: "/categories/upcoming", 30 | icon: , 31 | }, 32 | ]; 33 | 34 | return ( 35 |
36 |
37 | 38 |
39 |
40 | ); 41 | } 42 | 43 | export default Menu; 44 | -------------------------------------------------------------------------------- /app/components/movies/movie/movieDetailItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Genre, 3 | ProductionCompany, 4 | ProductionCountry, 5 | SpokenLanguage, 6 | } from "~/types/movies"; 7 | 8 | interface IMovieDetailItemProps { 9 | title: string; 10 | text?: string | number; 11 | array?: 12 | | Genre[] 13 | | ProductionCompany[] 14 | | ProductionCountry[] 15 | | SpokenLanguage[]; 16 | } 17 | 18 | export default function MovieDetailItem({ 19 | title, 20 | text = "", 21 | array, 22 | }: IMovieDetailItemProps) { 23 | return ( 24 |
25 | {title} 26 | {text ? ( 27 | <> 28 |

{text}

29 | 30 | ) : ( 31 | <> 32 | {array && ( 33 |

34 | {array.map((item, index) => { 35 | return ( 36 | {item.name} 37 | ); 38 | })} 39 |

40 | )} 41 | 42 | )} 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/components/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Header } from './layout/header'; 2 | export { default as Footer } from './layout/footer'; 3 | export { default as Main } from './layout/main'; 4 | export { default as Menu } from './layout/menu'; 5 | export { default as MenuItem } from './layout/menuItem'; 6 | export { default as Sidebar } from './layout/sidebar'; 7 | export { default as Error } from './shared/error'; 8 | export { default as MainTitle } from './shared/mainTitle'; 9 | export { default as SimilarMovies } from './shared/similarMovies'; 10 | export { default as CategorySection } from './sections/categorySection'; 11 | export { default as HeroSection } from './sections/heroSection'; 12 | export { default as MoviesCard } from './movies/moviesCard'; 13 | export { default as MoviesDetailModal } from './movies/moviesDetailModal'; 14 | export { default as MovieDetailHeader } from './movies/movie/movieDetailHeader'; 15 | export { default as MovieDetailItem } from './movies/movie/movieDetailItem'; 16 | export { default as MovieDetailsImages } from './movies/movie/movieDetailsImages'; 17 | export { default as MovieDetailButtons } from './movies/movie/movieDetailButtons'; 18 | 19 | -------------------------------------------------------------------------------- /app/components/sections/heroSection.tsx: -------------------------------------------------------------------------------- 1 | import {NavLink} from "remix"; 2 | 3 | function HeroSection({img, title, overview, id}: { img: string, title: string, overview: string, id: number }) { 4 | 5 | return ( 6 | 10 | Hero Section 11 |
12 |
13 |

{title}

14 |
15 |
16 |
17 |
18 |

{overview}

19 |
20 |
21 |
22 | ); 23 | } 24 | 25 | export default HeroSection; -------------------------------------------------------------------------------- /app/components/sections/categorySection.tsx: -------------------------------------------------------------------------------- 1 | import { IMoviesProps } from "~/types/movies"; 2 | import { HeroSection, MoviesCard, MainTitle } from "~/components"; 3 | 4 | interface ICategorySectionProps { 5 | title: string; 6 | movies: IMoviesProps[]; 7 | heroSectionMovie: IMoviesProps; 8 | } 9 | 10 | function CategorySection({ 11 | title, 12 | movies, 13 | heroSectionMovie, 14 | }: ICategorySectionProps) { 15 | const BASE_BACKDROP_PATH = "https://www.themoviedb.org/t/p/w1920_and_h800_multi_faces"; 16 | 17 | return ( 18 |
19 | 25 | 26 | 27 | 28 |
29 | {movies.map((movie, index) => ( 30 | 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | 37 | export default CategorySection; 38 | -------------------------------------------------------------------------------- /app/types/movies.ts: -------------------------------------------------------------------------------- 1 | export interface Genre { 2 | id: number; 3 | name: string; 4 | } 5 | 6 | export interface ProductionCompany { 7 | id: number; 8 | logo_path: string; 9 | name: string; 10 | origin_country: string; 11 | } 12 | 13 | export interface ProductionCountry { 14 | iso_3166_1: string; 15 | name: string; 16 | } 17 | 18 | export interface SpokenLanguage { 19 | english_name: string; 20 | iso_639_1: string; 21 | name: string; 22 | } 23 | 24 | export interface IMoviesProps { 25 | genre_ids: any; 26 | adult: boolean; 27 | backdrop_path: string; 28 | belongs_to_collection?: any; 29 | budget: number; 30 | genres: Genre[]; 31 | homepage: string; 32 | id: number; 33 | imdb_id: string; 34 | original_language: string; 35 | original_title: string; 36 | overview: string; 37 | popularity: number; 38 | poster_path: string; 39 | production_companies: ProductionCompany[]; 40 | production_countries: ProductionCountry[]; 41 | release_date: string; 42 | revenue: number; 43 | runtime: number; 44 | spoken_languages: SpokenLanguage[]; 45 | status: string; 46 | tagline: string; 47 | title: string; 48 | video: boolean; 49 | vote_average: number; 50 | vote_count: number; 51 | } -------------------------------------------------------------------------------- /app/components/layout/menuItem.tsx: -------------------------------------------------------------------------------- 1 | import { NavLink } from "remix"; 2 | 3 | interface MenuItemProps { 4 | links: MenuItemLinks[]; 5 | title: string; 6 | } 7 | 8 | interface MenuItemLinks { 9 | id: number | string; 10 | title: string; 11 | path: string; 12 | icon: JSX.Element; 13 | } 14 | 15 | function MenuItem(props: MenuItemProps) { 16 | return ( 17 | <> 18 | {props.title} 19 |
20 | {props.links.map((link, index) => ( 21 | 25 | isActive 26 | ? "flex items-center w-full px-3 py-2 hover:bg-brand hover:text-white dark:hover:bg-red-600 dark:hover:text-white bg-brand text-white dark:bg-red-600 rounded-md" 27 | : "flex items-center w-full px-3 py-2 hover:bg-brand hover:text-white dark:hover:bg-red-600 dark:hover:text-white rounded-md" 28 | } 29 | 30 | > 31 | {link.icon} 32 | {link.title} 33 | 34 | ))} 35 |
36 | 37 | ); 38 | } 39 | 40 | export default MenuItem; 41 | -------------------------------------------------------------------------------- /app/routes/categories/$categorySlug.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunction, useLoaderData } from "remix"; 2 | import invariant from "tiny-invariant"; 3 | import { getMoviesByCategory } from "~/services/apiService"; 4 | import { IMoviesProps } from "~/types/movies"; 5 | import { CategorySection } from "~/components"; 6 | 7 | export const loader: LoaderFunction = async ({ params }) => { 8 | invariant(params.categorySlug, "Category slug is required"); 9 | let { categorySlug } = params; 10 | let title = ""; 11 | 12 | switch (categorySlug) { 13 | case "popular": 14 | title = "Popular Movies"; 15 | break; 16 | case "top-rated": 17 | title = "Top Rated Movies"; 18 | categorySlug = "top_rated"; 19 | break; 20 | case "upcoming": 21 | title = "Upcoming Movies"; 22 | break; 23 | default: 24 | title = "Now Playing Movies"; 25 | break; 26 | } 27 | 28 | const { results } = await getMoviesByCategory(categorySlug); 29 | 30 | return { results, title }; 31 | }; 32 | 33 | function CategoryDetails() { 34 | const { results, title } = useLoaderData(); 35 | const movies: IMoviesProps[] = results; 36 | const heroSectionMovie = 37 | movies.find((movie) => movie.vote_average > 8) || movies[0]; 38 | return ( 39 | 44 | ); 45 | } 46 | 47 | export default CategoryDetails; 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Movie App with Remix.js 2 | 3 | Bu proje [Remix Run](https://remix.run/) , [Tailwind CSS](https://tailwindcss.com) ve [TMDB](https://www.themoviedb.org/) kullanılarak geliştirdiğim Remix.run film sitesi uygulamasıdır. 4 | 5 | ## Kullanılan paketler 6 | 7 |
    8 |
  • axios
  • 9 |
  • react-icons
  • 10 |
  • react-modals
  • 11 |
  • react-responsive-carousel
  • 12 |
  • react-youtube
  • 13 |
  • slugify
  • 14 |
  • tailwind-scrollbar
  • 15 |
  • tiny-invariant
  • 16 |
  • vanilla-tilt
  • 17 |
18 | 19 | ## Kurulum 20 | 21 | İlk olarak aşağıdaki komutu kopyalanız. Ardından terminal ekranını açarak, projenin kurulmasını istediğiniz bir alana gelerek yapıştırıp çalıştırınız. 22 | 23 | ``` 24 | git clone https://github.com/harundogdu/movie-app-with-remix.git 25 | ``` 26 | 27 |
28 | Klonlama işleminin ardından kurulum işlemleri için terminal ekranına projenin adını yazarak, aşağıda bulunan kodu yapıştırıp çalıştırınız. 29 | 30 | ``` 31 | cd movie-app-with-remix && npm install 32 | ``` 33 | 34 | ## Çalıştırma 35 | 36 | Projeyi çalıştırmak için ana dizine gelip, aşağıdaki komutu yazmanız yeterli olacaktır. 37 | ``` 38 | npm run dev 39 | ``` 40 | 41 | ## Live Demo 42 | 43 | Canlı Demoyu [Bura](https://movie-app-with-remix.vercel.app/)dan İnceleyebilirsiniz 44 | 45 | ## Daha fazlası 46 | 47 | Daha fazlası ve aklınıza takılan herhangi bir soru için için bana kişisel [web sitem](https://harundogdu.dev/) üzerinden ulaşabilir, "Pull Request" isteklerinde bulunabilirsiniz. 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remix-app-template", 3 | "private": true, 4 | "description": "", 5 | "license": "", 6 | "sideEffects": false, 7 | "scripts": { 8 | "build": "npm run build:css && cross-env NODE_ENV=production GENERATE_SOURCEMAP=false remix build", 9 | "build:css": "tailwindcss -o app/styles/app.css", 10 | "dev": "concurrently \"npm run dev:css\" \"cross-env NODE_ENV=development remix dev\"", 11 | "dev:css": "tailwindcss -o app/styles/app.css", 12 | "postinstall": "remix setup node" 13 | }, 14 | "dependencies": { 15 | "@remix-run/react": "^1.2.3", 16 | "@remix-run/vercel": "^1.2.3", 17 | "axios": "^0.26.1", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2", 20 | "react-icons": "^4.3.1", 21 | "react-modal": "^3.14.4", 22 | "react-responsive-carousel": "^3.2.23", 23 | "react-youtube": "^7.14.0", 24 | "remix": "^1.2.3", 25 | "slugify": "^1.6.5", 26 | "tailwind-scrollbar": "^1.3.1", 27 | "tiny-invariant": "^1.2.0", 28 | "vanilla-tilt": "^1.7.2" 29 | }, 30 | "devDependencies": { 31 | "@remix-run/dev": "^1.2.3", 32 | "@remix-run/eslint-config": "^1.2.3", 33 | "@remix-run/serve": "^1.2.3", 34 | "@types/react": "^17.0.24", 35 | "@types/react-dom": "^17.0.9", 36 | "@types/react-modal": "^3.13.1", 37 | "autoprefixer": "^10.4.4", 38 | "concurrently": "^7.0.0", 39 | "cross-env": "^7.0.3", 40 | "eslint": "^8.9.0", 41 | "postcss": "^8.4.12", 42 | "tailwindcss": "^3.0.23", 43 | "typescript": "^4.5.5" 44 | }, 45 | "engines": { 46 | "node": ">=14" 47 | } 48 | } -------------------------------------------------------------------------------- /app/services/apiService.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const API_KEY = process.env.API_KEY; 4 | 5 | export const apiService = axios.create({ 6 | baseURL: process.env.API_URL, 7 | headers: { 8 | "Content-Type": "application/json", 9 | "Accept": "application/json" 10 | } 11 | }); 12 | 13 | export const getMovie = async (movieId: string) => { 14 | const response = await apiService.get(`/movie/${movieId}?api_key=${API_KEY}`) 15 | return response.data; 16 | } 17 | 18 | export const getMovieVideos = async (movieId: string) => { 19 | const response = await apiService.get(`/movie/${movieId}/videos?api_key=${API_KEY}`); 20 | return response.data; 21 | } 22 | /* category fetching */ 23 | 24 | export const getMoviesByCategory = async (category: string) => { 25 | const response = await apiService.get(`/movie/${category}?api_key=${API_KEY}`); 26 | return response.data; 27 | } 28 | 29 | export const getNowPlayingMovies = async () => { 30 | const response = await apiService.get(`/movie/now_playing?api_key=${API_KEY}`); 31 | return response.data; 32 | } 33 | 34 | export const getSimilarMovies = async (movieId: string) => { 35 | const response = await apiService.get(`/movie/${movieId}/similar?api_key=${API_KEY}`); 36 | const { results } = response.data; 37 | return results; 38 | } 39 | 40 | /* Search Movie */ 41 | export const getSearchMovieDetails = async (query: string) => { 42 | const response = await apiService.get(`/search/movie?api_key=${API_KEY}&language=en-US&query=${query}&page=1`); 43 | const { results } = response.data; 44 | return results; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /app/routes/search/$searchQuery.tsx: -------------------------------------------------------------------------------- 1 | import { json, LoaderFunction, useLoaderData } from "remix"; 2 | import invariant from "tiny-invariant"; 3 | import { getSearchMovieDetails } from "~/services/apiService"; 4 | import { IMoviesProps } from "~/types/movies"; 5 | import { MainTitle, MoviesCard } from "~/components"; 6 | 7 | export const loader: LoaderFunction = async ({ params }) => { 8 | invariant(params.searchQuery, "params is required"); 9 | const searchQuery = params.searchQuery; 10 | return json(await getSearchMovieDetails(searchQuery)); 11 | } 12 | 13 | const SearchQuery = () => { 14 | const movies = useLoaderData(); 15 | return ( 16 |
17 | { 18 | movies.length > 0 ? 19 | ( 20 | <> 21 | 22 |
23 | {movies.map((movie, index) => ( 24 | 28 | ))} 29 |
30 | 31 | ) 32 | : 33 | ( 34 | 35 | ) 36 | } 37 |
38 | ); 39 | }; 40 | 41 | export default SearchQuery; -------------------------------------------------------------------------------- /app/components/movies/movie/movieDetailsImages.tsx: -------------------------------------------------------------------------------- 1 | import { Carousel } from "react-responsive-carousel"; 2 | import { IMoviesProps } from "~/types/movies"; 3 | import { MainTitle } from "~/components"; 4 | 5 | interface IMovieDetailsImagesProps { 6 | movie: IMoviesProps; 7 | } 8 | 9 | export default function MovieDetailsImages({ 10 | movie, 11 | }: IMovieDetailsImagesProps) { 12 | const BASE_BACKDROP_PATH = "https://www.themoviedb.org/t/p/w1920_and_h800_multi_faces" 13 | const BASE_POSTER_PATH = "https://www.themoviedb.org/t/p/w220_and_h330_face"; 14 | 15 | return ( 16 |
17 | 18 | 19 |
20 | {movie.title} 25 |
26 |
27 | {movie.title} 32 |
33 | {movie.belongs_to_collection ? ( 34 |
35 | {movie.title} 40 |
41 | ) : ( 42 | <> 43 | )} 44 | {movie.belongs_to_collection ? ( 45 |
46 | {movie.title} 51 |
52 | ) : ( 53 | <> 54 | )} 55 |
56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /app/components/layout/footer.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineGithub } from "react-icons/ai"; 2 | import { NavLink } from "remix"; 3 | import Logo from "~/assets/logo.png"; 4 | 5 | const Footer = () => { 6 | return ( 7 |
8 | 9 | Sidebar 10 | 11 | To The Best 12 | 13 | 14 |
15 |

copyright © {new Date().getFullYear()}

16 | 17 | 23 | 27 | 28 | 34 | 38 | 39 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default Footer; 46 | -------------------------------------------------------------------------------- /app/components/movies/moviesCard.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { NavLink } from "remix"; 3 | import VanillaTilt from 'vanilla-tilt'; 4 | import { AiFillStar, AiOutlineStar } from "react-icons/ai"; 5 | import { IMoviesProps } from "~/types/movies"; 6 | 7 | function MoviesCard({ movie }: { movie: IMoviesProps }) { 8 | console.log('re_render'); 9 | 10 | const BASE_POSTER_PATH = "https://www.themoviedb.org/t/p/w220_and_h330_face"; 11 | 12 | const calculateStars = (star: number) => { 13 | const stars = []; 14 | for (let i = 0; i <= star / 2; i++) { 15 | stars.push(); 16 | } 17 | for (let i = 0; i < 4 - star / 2; i++) { 18 | stars.push(); 19 | } 20 | return stars; 21 | } 22 | 23 | const tilt = useRef(null); 24 | // eslint-disable-next-line react-hooks/exhaustive-deps 25 | const options = { 26 | speed: 300, 27 | max: 10, 28 | perspective: 1000, 29 | transition: true, 30 | easing: "cubic-bezier(.03,.98,.52,.99)" 31 | } 32 | 33 | useEffect(() => { 34 | // @ts-ignore 35 | return VanillaTilt.init(tilt.current, options); 36 | }, [options]); 37 | 38 | 39 | return ( 40 | 44 |
45 |
46 | {movie.title} 51 |

52 | {movie.title} 53 |

54 |
55 |
56 | {calculateStars(movie.vote_average)} 57 |
58 | 59 |
60 | 61 |
62 | ); 63 | } 64 | 65 | export default MoviesCard; 66 | -------------------------------------------------------------------------------- /app/routes/movie/$movieId.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | LinksFunction, 4 | LoaderFunction, 5 | MetaFunction, 6 | useLoaderData, 7 | } from "remix"; 8 | import invariant from "tiny-invariant"; 9 | import styles from "react-responsive-carousel/lib/styles/carousel.min.css"; 10 | import Modal from "react-modal"; 11 | import { 12 | getMovie, 13 | getMovieVideos, 14 | getSimilarMovies, 15 | } from "~/services/apiService"; 16 | import { customModalStyles } from "~/helpers/utils"; 17 | import { Result } from "~/types/videos"; 18 | import { MovieDetailHeader, MovieDetailsImages, MoviesDetailModal, SimilarMovies } from "~/components"; 19 | 20 | export const meta: MetaFunction = ({ data }) => { 21 | const movie = data.movie; 22 | invariant(movie, "movie is required"); 23 | return { 24 | title: `${movie.title}`, 25 | description: `${movie.overview}`, 26 | }; 27 | }; 28 | 29 | export const links: LinksFunction = () => { 30 | return [{ rel: "stylesheet", href: styles }]; 31 | }; 32 | 33 | export const loader: LoaderFunction = async ({ params }) => { 34 | invariant(params.movieId, "params.movieId is required"); 35 | const movie = await getMovie(params.movieId); 36 | 37 | if (!movie) { 38 | throw new Error("Movie not found"); 39 | } 40 | 41 | const videos = await getMovieVideos(params.movieId); 42 | const similarMovies = await getSimilarMovies(params.movieId); 43 | 44 | return { 45 | movie, 46 | videos, 47 | similarMovies, 48 | }; 49 | }; 50 | 51 | export default function MovieDetail() { 52 | Modal.setAppElement("body"); 53 | 54 | const { movie, videos, similarMovies } = useLoaderData(); 55 | const [modalIsOpen, setModalIsOpen] = React.useState(false); 56 | 57 | const currentVideo = videos.results.find( 58 | (video: Result) => 59 | video.name.includes("Official Trailer") || video.name.includes("Trailer") 60 | ); 61 | 62 | return ( 63 |
64 | {/* Details */} 65 | 69 | {/* Images */} 70 | 73 | {/* Modal */} 74 | 80 | {/* Similar Movies */} 81 | 82 |
83 | ); 84 | } -------------------------------------------------------------------------------- /app/components/layout/header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Form, useSubmit } from "remix"; 3 | import { BsFillMoonFill } from "react-icons/bs"; 4 | import { GiHamburgerMenu } from "react-icons/gi"; 5 | 6 | const Header = () => { 7 | const submit = useSubmit(); 8 | 9 | const [darkMode, setDarkMode] = React.useState(false); 10 | const [searchQuery, setSearchQuery] = React.useState(""); 11 | 12 | const toggleTheme = () => { 13 | localStorage.setItem("dark", JSON.stringify(!darkMode)); 14 | setDarkMode(!darkMode); 15 | }; 16 | 17 | const handleSearchQuerySubmit = (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | setSearchQuery(""); 20 | submit(null, { method: "get", action: `/search/${searchQuery}` }); 21 | }; 22 | 23 | const handleSidebarVisibleState = () => { 24 | document.querySelector("#sidebar")?.classList.toggle("hidden"); 25 | document.querySelector("#sidebar")?.classList.toggle("flex"); 26 | } 27 | 28 | React.useEffect(() => { 29 | const root = document.documentElement; 30 | 31 | if (darkMode) { 32 | root.classList.add("dark"); 33 | } else { 34 | root.classList.remove("dark"); 35 | } 36 | 37 | }, [darkMode]); 38 | 39 | React.useEffect(() => { 40 | const localMode = JSON.parse(localStorage.getItem("dark") || "false"); 41 | setDarkMode(localMode); 42 | }, []); 43 | 44 | return ( 45 |
46 | 47 | 50 | 51 |
52 |
53 | 59 |
60 | 61 |
62 | setSearchQuery(e.target.value)} 68 | /> 69 |
70 |
71 |
72 | ); 73 | }; 74 | 75 | export default Header; 76 | -------------------------------------------------------------------------------- /app/root.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Links, 3 | LinksFunction, 4 | LiveReload, 5 | Meta, 6 | MetaFunction, 7 | Outlet, 8 | Scripts, 9 | ScrollRestoration, 10 | useCatch, 11 | Link 12 | } from "remix"; 13 | import styles from "~/styles/app.css"; 14 | import mainStyles from "~/styles/main.css"; 15 | import { IDocumentProps, IErrorProps, ILayoutProps } from "~/types/root"; 16 | import { Error, Footer, Header, Main, Sidebar } from "~/components"; 17 | 18 | export const meta: MetaFunction = () => { 19 | return { title: "HD Movie App | Homepage" }; 20 | }; 21 | 22 | export const links: LinksFunction = () => { 23 | return [ 24 | { rel: "stylesheet", href: styles }, 25 | { rel: "stylesheet", href: mainStyles } 26 | ]; 27 | }; 28 | 29 | export default function App() { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | export function Document({ children, title }: IDocumentProps) { 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | {title || "HD Movie App | Homepage"} 48 | 49 | 50 | {children} 51 | 52 | 53 | {process.env.NODE_ENV === "development" && } 54 | 55 | 56 | ); 57 | } 58 | 59 | export function Layout({ children }: ILayoutProps) { 60 | return ( 61 |
62 | <> 63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | ); 74 | } 75 | 76 | export function ErrorBoundary({ error }: IErrorProps) { 77 | return ( 78 | 79 | 80 | 81 | 82 | ); 83 | } 84 | 85 | 86 | export function CatchBoundary() { 87 | let caught = useCatch(); 88 | 89 | let message; 90 | switch (caught.status) { 91 | case 401: 92 | message = ( 93 |

94 | Oops! Looks like you tried to visit a page that you do not have access 95 | to. 96 |

97 | ); 98 | break; 99 | case 404: 100 | message = ( 101 |

Oops! Looks like you tried to visit a page that does not exist.

102 | ); 103 | break; 104 | default: 105 | message = ( 106 |

107 | Oops! Something went wrong. Please try again later or contact us if the problem persists. 108 |

109 | ); 110 | } 111 | 112 | return ( 113 | 114 |
115 |
116 | {caught.status} 117 |
118 |

119 | You're alone here 120 |

121 |

122 | {message} 123 |

124 | 125 | Go back Home 126 | 127 |
128 |
129 | ); 130 | } -------------------------------------------------------------------------------- /app/components/movies/movie/movieDetailHeader.tsx: -------------------------------------------------------------------------------- 1 | import { HiExternalLink } from "react-icons/hi"; 2 | import { IMoviesProps } from "~/types/movies"; 3 | import { MovieDetailItem, MovieDetailButtons } from '~/components'; 4 | 5 | interface MovieDetailHeaderProps { 6 | movie: IMoviesProps; 7 | setModalIsOpen: (isOpen: boolean) => void; 8 | } 9 | 10 | export default function MovieDetailHeader({ 11 | movie, 12 | setModalIsOpen 13 | }: MovieDetailHeaderProps) { 14 | const BASE_BACKDROP_PATH = "https://www.themoviedb.org/t/p/w1920_and_h800_multi_faces"; 15 | const BASE_POSTER_PATH = "https://www.themoviedb.org/t/p/w220_and_h330_face" 16 | 17 | const parseYear = (date: string) => { 18 | return date.split("-")[0]; 19 | }; 20 | return ( 21 |
22 |
30 | 31 |
33 |
34 | {movie.title} 39 | 44 |
45 |
46 |

{movie.title} - ({parseYear(movie.release_date)}) 47 | {movie.adult ? "- +18" : ""} 48 | {movie.status} 49 |

50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | 61 | 62 | 63 |
64 | 65 |
66 | 67 | 68 | 69 | Load More.. 70 | 71 | 72 |
73 | 74 |
75 | 79 | 83 |
84 | 85 |
86 |
87 |
88 | 89 | ); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /app/styles/app.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | */ 34 | 35 | html { 36 | line-height: 1.5; 37 | /* 1 */ 38 | -webkit-text-size-adjust: 100%; 39 | /* 2 */ 40 | -moz-tab-size: 4; 41 | /* 3 */ 42 | -o-tab-size: 4; 43 | tab-size: 4; 44 | /* 3 */ 45 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 46 | /* 4 */ 47 | } 48 | 49 | /* 50 | 1. Remove the margin in all browsers. 51 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 52 | */ 53 | 54 | body { 55 | margin: 0; 56 | /* 1 */ 57 | line-height: inherit; 58 | /* 2 */ 59 | } 60 | 61 | /* 62 | 1. Add the correct height in Firefox. 63 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 64 | 3. Ensure horizontal rules are visible by default. 65 | */ 66 | 67 | hr { 68 | height: 0; 69 | /* 1 */ 70 | color: inherit; 71 | /* 2 */ 72 | border-top-width: 1px; 73 | /* 3 */ 74 | } 75 | 76 | /* 77 | Add the correct text decoration in Chrome, Edge, and Safari. 78 | */ 79 | 80 | abbr:where([title]) { 81 | -webkit-text-decoration: underline dotted; 82 | text-decoration: underline dotted; 83 | } 84 | 85 | /* 86 | Remove the default font size and weight for headings. 87 | */ 88 | 89 | h1, 90 | h2, 91 | h3, 92 | h4, 93 | h5, 94 | h6 { 95 | font-size: inherit; 96 | font-weight: inherit; 97 | } 98 | 99 | /* 100 | Reset links to optimize for opt-in styling instead of opt-out. 101 | */ 102 | 103 | a { 104 | color: inherit; 105 | text-decoration: inherit; 106 | } 107 | 108 | /* 109 | Add the correct font weight in Edge and Safari. 110 | */ 111 | 112 | b, 113 | strong { 114 | font-weight: bolder; 115 | } 116 | 117 | /* 118 | 1. Use the user's configured `mono` font family by default. 119 | 2. Correct the odd `em` font sizing in all browsers. 120 | */ 121 | 122 | code, 123 | kbd, 124 | samp, 125 | pre { 126 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 127 | /* 1 */ 128 | font-size: 1em; 129 | /* 2 */ 130 | } 131 | 132 | /* 133 | Add the correct font size in all browsers. 134 | */ 135 | 136 | small { 137 | font-size: 80%; 138 | } 139 | 140 | /* 141 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 142 | */ 143 | 144 | sub, 145 | sup { 146 | font-size: 75%; 147 | line-height: 0; 148 | position: relative; 149 | vertical-align: baseline; 150 | } 151 | 152 | sub { 153 | bottom: -0.25em; 154 | } 155 | 156 | sup { 157 | top: -0.5em; 158 | } 159 | 160 | /* 161 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 162 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 163 | 3. Remove gaps between table borders by default. 164 | */ 165 | 166 | table { 167 | text-indent: 0; 168 | /* 1 */ 169 | border-color: inherit; 170 | /* 2 */ 171 | border-collapse: collapse; 172 | /* 3 */ 173 | } 174 | 175 | /* 176 | 1. Change the font styles in all browsers. 177 | 2. Remove the margin in Firefox and Safari. 178 | 3. Remove default padding in all browsers. 179 | */ 180 | 181 | button, 182 | input, 183 | optgroup, 184 | select, 185 | textarea { 186 | font-family: inherit; 187 | /* 1 */ 188 | font-size: 100%; 189 | /* 1 */ 190 | line-height: inherit; 191 | /* 1 */ 192 | color: inherit; 193 | /* 1 */ 194 | margin: 0; 195 | /* 2 */ 196 | padding: 0; 197 | /* 3 */ 198 | } 199 | 200 | /* 201 | Remove the inheritance of text transform in Edge and Firefox. 202 | */ 203 | 204 | button, 205 | select { 206 | text-transform: none; 207 | } 208 | 209 | /* 210 | 1. Correct the inability to style clickable types in iOS and Safari. 211 | 2. Remove default button styles. 212 | */ 213 | 214 | button, 215 | [type='button'], 216 | [type='reset'], 217 | [type='submit'] { 218 | -webkit-appearance: button; 219 | /* 1 */ 220 | background-color: transparent; 221 | /* 2 */ 222 | background-image: none; 223 | /* 2 */ 224 | } 225 | 226 | /* 227 | Use the modern Firefox focus style for all focusable elements. 228 | */ 229 | 230 | :-moz-focusring { 231 | outline: auto; 232 | } 233 | 234 | /* 235 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 236 | */ 237 | 238 | :-moz-ui-invalid { 239 | box-shadow: none; 240 | } 241 | 242 | /* 243 | Add the correct vertical alignment in Chrome and Firefox. 244 | */ 245 | 246 | progress { 247 | vertical-align: baseline; 248 | } 249 | 250 | /* 251 | Correct the cursor style of increment and decrement buttons in Safari. 252 | */ 253 | 254 | ::-webkit-inner-spin-button, 255 | ::-webkit-outer-spin-button { 256 | height: auto; 257 | } 258 | 259 | /* 260 | 1. Correct the odd appearance in Chrome and Safari. 261 | 2. Correct the outline style in Safari. 262 | */ 263 | 264 | [type='search'] { 265 | -webkit-appearance: textfield; 266 | /* 1 */ 267 | outline-offset: -2px; 268 | /* 2 */ 269 | } 270 | 271 | /* 272 | Remove the inner padding in Chrome and Safari on macOS. 273 | */ 274 | 275 | ::-webkit-search-decoration { 276 | -webkit-appearance: none; 277 | } 278 | 279 | /* 280 | 1. Correct the inability to style clickable types in iOS and Safari. 281 | 2. Change font properties to `inherit` in Safari. 282 | */ 283 | 284 | ::-webkit-file-upload-button { 285 | -webkit-appearance: button; 286 | /* 1 */ 287 | font: inherit; 288 | /* 2 */ 289 | } 290 | 291 | /* 292 | Add the correct display in Chrome and Safari. 293 | */ 294 | 295 | summary { 296 | display: list-item; 297 | } 298 | 299 | /* 300 | Removes the default spacing and border for appropriate elements. 301 | */ 302 | 303 | blockquote, 304 | dl, 305 | dd, 306 | h1, 307 | h2, 308 | h3, 309 | h4, 310 | h5, 311 | h6, 312 | hr, 313 | figure, 314 | p, 315 | pre { 316 | margin: 0; 317 | } 318 | 319 | fieldset { 320 | margin: 0; 321 | padding: 0; 322 | } 323 | 324 | legend { 325 | padding: 0; 326 | } 327 | 328 | ol, 329 | ul, 330 | menu { 331 | list-style: none; 332 | margin: 0; 333 | padding: 0; 334 | } 335 | 336 | /* 337 | Prevent resizing textareas horizontally by default. 338 | */ 339 | 340 | textarea { 341 | resize: vertical; 342 | } 343 | 344 | /* 345 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 346 | 2. Set the default placeholder color to the user's configured gray 400 color. 347 | */ 348 | 349 | input::-moz-placeholder, textarea::-moz-placeholder { 350 | opacity: 1; 351 | /* 1 */ 352 | color: #9ca3af; 353 | /* 2 */ 354 | } 355 | 356 | input:-ms-input-placeholder, textarea:-ms-input-placeholder { 357 | opacity: 1; 358 | /* 1 */ 359 | color: #9ca3af; 360 | /* 2 */ 361 | } 362 | 363 | input::placeholder, 364 | textarea::placeholder { 365 | opacity: 1; 366 | /* 1 */ 367 | color: #9ca3af; 368 | /* 2 */ 369 | } 370 | 371 | /* 372 | Set the default cursor for buttons. 373 | */ 374 | 375 | button, 376 | [role="button"] { 377 | cursor: pointer; 378 | } 379 | 380 | /* 381 | Make sure disabled buttons don't get the pointer cursor. 382 | */ 383 | 384 | :disabled { 385 | cursor: default; 386 | } 387 | 388 | /* 389 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 390 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 391 | This can trigger a poorly considered lint error in some tools but is included by design. 392 | */ 393 | 394 | img, 395 | svg, 396 | video, 397 | canvas, 398 | audio, 399 | iframe, 400 | embed, 401 | object { 402 | display: block; 403 | /* 1 */ 404 | vertical-align: middle; 405 | /* 2 */ 406 | } 407 | 408 | /* 409 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 410 | */ 411 | 412 | img, 413 | video { 414 | max-width: 100%; 415 | height: auto; 416 | } 417 | 418 | /* 419 | Ensure the default browser behavior of the `hidden` attribute. 420 | */ 421 | 422 | [hidden] { 423 | display: none; 424 | } 425 | 426 | * { 427 | scrollbar-color: initial; 428 | scrollbar-width: initial; 429 | } 430 | 431 | *, ::before, ::after { 432 | --tw-translate-x: 0; 433 | --tw-translate-y: 0; 434 | --tw-rotate: 0; 435 | --tw-skew-x: 0; 436 | --tw-skew-y: 0; 437 | --tw-scale-x: 1; 438 | --tw-scale-y: 1; 439 | --tw-pan-x: ; 440 | --tw-pan-y: ; 441 | --tw-pinch-zoom: ; 442 | --tw-scroll-snap-strictness: proximity; 443 | --tw-ordinal: ; 444 | --tw-slashed-zero: ; 445 | --tw-numeric-figure: ; 446 | --tw-numeric-spacing: ; 447 | --tw-numeric-fraction: ; 448 | --tw-ring-inset: ; 449 | --tw-ring-offset-width: 0px; 450 | --tw-ring-offset-color: #fff; 451 | --tw-ring-color: rgb(59 130 246 / 0.5); 452 | --tw-ring-offset-shadow: 0 0 #0000; 453 | --tw-ring-shadow: 0 0 #0000; 454 | --tw-shadow: 0 0 #0000; 455 | --tw-shadow-colored: 0 0 #0000; 456 | --tw-blur: ; 457 | --tw-brightness: ; 458 | --tw-contrast: ; 459 | --tw-grayscale: ; 460 | --tw-hue-rotate: ; 461 | --tw-invert: ; 462 | --tw-saturate: ; 463 | --tw-sepia: ; 464 | --tw-drop-shadow: ; 465 | --tw-backdrop-blur: ; 466 | --tw-backdrop-brightness: ; 467 | --tw-backdrop-contrast: ; 468 | --tw-backdrop-grayscale: ; 469 | --tw-backdrop-hue-rotate: ; 470 | --tw-backdrop-invert: ; 471 | --tw-backdrop-opacity: ; 472 | --tw-backdrop-saturate: ; 473 | --tw-backdrop-sepia: ; 474 | } 475 | 476 | .absolute { 477 | position: absolute; 478 | } 479 | 480 | .relative { 481 | position: relative; 482 | } 483 | 484 | .inset-0 { 485 | top: 0px; 486 | right: 0px; 487 | bottom: 0px; 488 | left: 0px; 489 | } 490 | 491 | .-bottom-2 { 492 | bottom: -0.5rem; 493 | } 494 | 495 | .left-1\/2 { 496 | left: 50%; 497 | } 498 | 499 | .bottom-0 { 500 | bottom: 0px; 501 | } 502 | 503 | .left-0 { 504 | left: 0px; 505 | } 506 | 507 | .right-8 { 508 | right: 2rem; 509 | } 510 | 511 | .left-8 { 512 | left: 2rem; 513 | } 514 | 515 | .z-30 { 516 | z-index: 30; 517 | } 518 | 519 | .order-2 { 520 | order: 2; 521 | } 522 | 523 | .order-1 { 524 | order: 1; 525 | } 526 | 527 | .m-2 { 528 | margin: 0.5rem; 529 | } 530 | 531 | .m-auto { 532 | margin: auto; 533 | } 534 | 535 | .mx-1 { 536 | margin-left: 0.25rem; 537 | margin-right: 0.25rem; 538 | } 539 | 540 | .my-4 { 541 | margin-top: 1rem; 542 | margin-bottom: 1rem; 543 | } 544 | 545 | .my-3 { 546 | margin-top: 0.75rem; 547 | margin-bottom: 0.75rem; 548 | } 549 | 550 | .my-10 { 551 | margin-top: 2.5rem; 552 | margin-bottom: 2.5rem; 553 | } 554 | 555 | .mx-auto { 556 | margin-left: auto; 557 | margin-right: auto; 558 | } 559 | 560 | .mt-4 { 561 | margin-top: 1rem; 562 | } 563 | 564 | .mr-4 { 565 | margin-right: 1rem; 566 | } 567 | 568 | .ml-4 { 569 | margin-left: 1rem; 570 | } 571 | 572 | .mb-2 { 573 | margin-bottom: 0.5rem; 574 | } 575 | 576 | .mr-1 { 577 | margin-right: 0.25rem; 578 | } 579 | 580 | .block { 581 | display: block; 582 | } 583 | 584 | .inline-block { 585 | display: inline-block; 586 | } 587 | 588 | .flex { 589 | display: flex; 590 | } 591 | 592 | .inline-flex { 593 | display: inline-flex; 594 | } 595 | 596 | .grid { 597 | display: grid; 598 | } 599 | 600 | .hidden { 601 | display: none; 602 | } 603 | 604 | .h-screen { 605 | height: 100vh; 606 | } 607 | 608 | .h-full { 609 | height: 100%; 610 | } 611 | 612 | .h-10 { 613 | height: 2.5rem; 614 | } 615 | 616 | .\!h-full { 617 | height: 100% !important; 618 | } 619 | 620 | .h-96 { 621 | height: 24rem; 622 | } 623 | 624 | .w-full { 625 | width: 100%; 626 | } 627 | 628 | .w-48 { 629 | width: 12rem; 630 | } 631 | 632 | .w-1\/2 { 633 | width: 50%; 634 | } 635 | 636 | .w-96 { 637 | width: 24rem; 638 | } 639 | 640 | .w-fit { 641 | width: -webkit-fit-content; 642 | width: -moz-fit-content; 643 | width: fit-content; 644 | } 645 | 646 | .w-5\/6 { 647 | width: 83.333333%; 648 | } 649 | 650 | .flex-1 { 651 | flex: 1 1 0%; 652 | } 653 | 654 | .flex-shrink-0 { 655 | flex-shrink: 0; 656 | } 657 | 658 | .-translate-x-1\/2 { 659 | --tw-translate-x: -50%; 660 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 661 | } 662 | 663 | .transform { 664 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); 665 | } 666 | 667 | .cursor-pointer { 668 | cursor: pointer; 669 | } 670 | 671 | .appearance-none { 672 | -webkit-appearance: none; 673 | -moz-appearance: none; 674 | appearance: none; 675 | } 676 | 677 | .grid-cols-1 { 678 | grid-template-columns: repeat(1, minmax(0, 1fr)); 679 | } 680 | 681 | .grid-cols-5 { 682 | grid-template-columns: repeat(5, minmax(0, 1fr)); 683 | } 684 | 685 | .flex-col { 686 | flex-direction: column; 687 | } 688 | 689 | .items-end { 690 | align-items: flex-end; 691 | } 692 | 693 | .items-center { 694 | align-items: center; 695 | } 696 | 697 | .justify-end { 698 | justify-content: flex-end; 699 | } 700 | 701 | .justify-center { 702 | justify-content: center; 703 | } 704 | 705 | .justify-between { 706 | justify-content: space-between; 707 | } 708 | 709 | .space-y-6 > :not([hidden]) ~ :not([hidden]) { 710 | --tw-space-y-reverse: 0; 711 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); 712 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); 713 | } 714 | 715 | .space-y-4 > :not([hidden]) ~ :not([hidden]) { 716 | --tw-space-y-reverse: 0; 717 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 718 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 719 | } 720 | 721 | .space-y-1 > :not([hidden]) ~ :not([hidden]) { 722 | --tw-space-y-reverse: 0; 723 | margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); 724 | margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); 725 | } 726 | 727 | .space-x-2 > :not([hidden]) ~ :not([hidden]) { 728 | --tw-space-x-reverse: 0; 729 | margin-right: calc(0.5rem * var(--tw-space-x-reverse)); 730 | margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); 731 | } 732 | 733 | .space-x-4 > :not([hidden]) ~ :not([hidden]) { 734 | --tw-space-x-reverse: 0; 735 | margin-right: calc(1rem * var(--tw-space-x-reverse)); 736 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); 737 | } 738 | 739 | .overflow-auto { 740 | overflow: auto; 741 | } 742 | 743 | .overflow-hidden { 744 | overflow: hidden; 745 | } 746 | 747 | .overflow-x-hidden { 748 | overflow-x: hidden; 749 | } 750 | 751 | .rounded { 752 | border-radius: 0.25rem; 753 | } 754 | 755 | .rounded-md { 756 | border-radius: 0.375rem; 757 | } 758 | 759 | .rounded-lg { 760 | border-radius: 0.5rem; 761 | } 762 | 763 | .rounded-t { 764 | border-top-left-radius: 0.25rem; 765 | border-top-right-radius: 0.25rem; 766 | } 767 | 768 | .rounded-t-lg { 769 | border-top-left-radius: 0.5rem; 770 | border-top-right-radius: 0.5rem; 771 | } 772 | 773 | .rounded-bl { 774 | border-bottom-left-radius: 0.25rem; 775 | } 776 | 777 | .rounded-br { 778 | border-bottom-right-radius: 0.25rem; 779 | } 780 | 781 | .rounded-tr-md { 782 | border-top-right-radius: 0.375rem; 783 | } 784 | 785 | .border { 786 | border-width: 1px; 787 | } 788 | 789 | .border-gray-800 { 790 | --tw-border-opacity: 1; 791 | border-color: rgb(31 41 55 / var(--tw-border-opacity)); 792 | } 793 | 794 | .bg-red-600 { 795 | --tw-bg-opacity: 1; 796 | background-color: rgb(220 38 38 / var(--tw-bg-opacity)); 797 | } 798 | 799 | .bg-gray-50 { 800 | --tw-bg-opacity: 1; 801 | background-color: rgb(249 250 251 / var(--tw-bg-opacity)); 802 | } 803 | 804 | .bg-brand { 805 | --tw-bg-opacity: 1; 806 | background-color: rgb(5 15 44 / var(--tw-bg-opacity)); 807 | } 808 | 809 | .bg-white { 810 | --tw-bg-opacity: 1; 811 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 812 | } 813 | 814 | .bg-black { 815 | --tw-bg-opacity: 1; 816 | background-color: rgb(0 0 0 / var(--tw-bg-opacity)); 817 | } 818 | 819 | .bg-brandYellow { 820 | --tw-bg-opacity: 1; 821 | background-color: rgb(241 208 10 / var(--tw-bg-opacity)); 822 | } 823 | 824 | .bg-opacity-40 { 825 | --tw-bg-opacity: 0.4; 826 | } 827 | 828 | .bg-gradient-to-b { 829 | background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); 830 | } 831 | 832 | .from-detailPrimary { 833 | --tw-gradient-from: rgba(61.5, 10.5, 10.5, .5); 834 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgb(61.5 10.5 10.5 / 0)); 835 | } 836 | 837 | .to-detailSecondary { 838 | --tw-gradient-to: rgba(71.5, 10.5, 10.5, 0.34); 839 | } 840 | 841 | .object-contain { 842 | -o-object-fit: contain; 843 | object-fit: contain; 844 | } 845 | 846 | .object-cover { 847 | -o-object-fit: cover; 848 | object-fit: cover; 849 | } 850 | 851 | .p-4 { 852 | padding: 1rem; 853 | } 854 | 855 | .p-6 { 856 | padding: 1.5rem; 857 | } 858 | 859 | .px-8 { 860 | padding-left: 2rem; 861 | padding-right: 2rem; 862 | } 863 | 864 | .py-2 { 865 | padding-top: 0.5rem; 866 | padding-bottom: 0.5rem; 867 | } 868 | 869 | .px-5 { 870 | padding-left: 1.25rem; 871 | padding-right: 1.25rem; 872 | } 873 | 874 | .py-8 { 875 | padding-top: 2rem; 876 | padding-bottom: 2rem; 877 | } 878 | 879 | .px-6 { 880 | padding-left: 1.5rem; 881 | padding-right: 1.5rem; 882 | } 883 | 884 | .px-4 { 885 | padding-left: 1rem; 886 | padding-right: 1rem; 887 | } 888 | 889 | .px-2 { 890 | padding-left: 0.5rem; 891 | padding-right: 0.5rem; 892 | } 893 | 894 | .px-3 { 895 | padding-left: 0.75rem; 896 | padding-right: 0.75rem; 897 | } 898 | 899 | .py-12 { 900 | padding-top: 3rem; 901 | padding-bottom: 3rem; 902 | } 903 | 904 | .py-5 { 905 | padding-top: 1.25rem; 906 | padding-bottom: 1.25rem; 907 | } 908 | 909 | .text-center { 910 | text-align: center; 911 | } 912 | 913 | .text-6xl { 914 | font-size: 3.75rem; 915 | line-height: 1; 916 | } 917 | 918 | .text-5xl { 919 | font-size: 3rem; 920 | line-height: 1; 921 | } 922 | 923 | .text-xs { 924 | font-size: 0.75rem; 925 | line-height: 1rem; 926 | } 927 | 928 | .text-sm { 929 | font-size: 0.875rem; 930 | line-height: 1.25rem; 931 | } 932 | 933 | .text-lg { 934 | font-size: 1.125rem; 935 | line-height: 1.75rem; 936 | } 937 | 938 | .text-2xl { 939 | font-size: 1.5rem; 940 | line-height: 2rem; 941 | } 942 | 943 | .text-4xl { 944 | font-size: 2.25rem; 945 | line-height: 2.5rem; 946 | } 947 | 948 | .text-xl { 949 | font-size: 1.25rem; 950 | line-height: 1.75rem; 951 | } 952 | 953 | .font-semibold { 954 | font-weight: 600; 955 | } 956 | 957 | .font-bold { 958 | font-weight: 700; 959 | } 960 | 961 | .leading-normal { 962 | line-height: 1.5; 963 | } 964 | 965 | .text-primary { 966 | --tw-text-opacity: 1; 967 | color: rgb(0 17 44 / var(--tw-text-opacity)); 968 | } 969 | 970 | .text-white { 971 | --tw-text-opacity: 1; 972 | color: rgb(255 255 255 / var(--tw-text-opacity)); 973 | } 974 | 975 | .text-gray-600 { 976 | --tw-text-opacity: 1; 977 | color: rgb(75 85 99 / var(--tw-text-opacity)); 978 | } 979 | 980 | .text-brandYellow { 981 | --tw-text-opacity: 1; 982 | color: rgb(241 208 10 / var(--tw-text-opacity)); 983 | } 984 | 985 | .text-gray-200 { 986 | --tw-text-opacity: 1; 987 | color: rgb(229 231 235 / var(--tw-text-opacity)); 988 | } 989 | 990 | .opacity-75 { 991 | opacity: 0.75; 992 | } 993 | 994 | .opacity-10 { 995 | opacity: 0.1; 996 | } 997 | 998 | .shadow-xl { 999 | --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 1000 | --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); 1001 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1002 | } 1003 | 1004 | .shadow { 1005 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 1006 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); 1007 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); 1008 | } 1009 | 1010 | .outline-none { 1011 | outline: 2px solid transparent; 1012 | outline-offset: 2px; 1013 | } 1014 | 1015 | .outline { 1016 | outline-style: solid; 1017 | } 1018 | 1019 | .transition-colors { 1020 | transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; 1021 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; 1022 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; 1023 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1024 | transition-duration: 150ms; 1025 | } 1026 | 1027 | .transition-all { 1028 | transition-property: all; 1029 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1030 | transition-duration: 150ms; 1031 | } 1032 | 1033 | .transition { 1034 | transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; 1035 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; 1036 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; 1037 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1038 | transition-duration: 150ms; 1039 | } 1040 | 1041 | .transition-transform { 1042 | transition-property: transform; 1043 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); 1044 | transition-duration: 150ms; 1045 | } 1046 | 1047 | .duration-300 { 1048 | transition-duration: 300ms; 1049 | } 1050 | 1051 | .scrollbar { 1052 | --scrollbar-track: initial; 1053 | --scrollbar-thumb: initial; 1054 | scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); 1055 | overflow: overlay; 1056 | } 1057 | 1058 | .scrollbar.overflow-x-hidden { 1059 | overflow-x: hidden; 1060 | } 1061 | 1062 | .scrollbar.overflow-y-hidden { 1063 | overflow-y: hidden; 1064 | } 1065 | 1066 | .scrollbar::-webkit-scrollbar-track { 1067 | background-color: var(--scrollbar-track); 1068 | } 1069 | 1070 | .scrollbar::-webkit-scrollbar-thumb { 1071 | background-color: var(--scrollbar-thumb); 1072 | } 1073 | 1074 | .scrollbar { 1075 | scrollbar-width: auto; 1076 | } 1077 | 1078 | .scrollbar::-webkit-scrollbar { 1079 | width: 16px; 1080 | height: 16px; 1081 | } 1082 | 1083 | .scrollbar-thin { 1084 | --scrollbar-track: initial; 1085 | --scrollbar-thumb: initial; 1086 | scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); 1087 | overflow: overlay; 1088 | } 1089 | 1090 | .scrollbar-thin.overflow-x-hidden { 1091 | overflow-x: hidden; 1092 | } 1093 | 1094 | .scrollbar-thin.overflow-y-hidden { 1095 | overflow-y: hidden; 1096 | } 1097 | 1098 | .scrollbar-thin::-webkit-scrollbar-track { 1099 | background-color: var(--scrollbar-track); 1100 | } 1101 | 1102 | .scrollbar-thin::-webkit-scrollbar-thumb { 1103 | background-color: var(--scrollbar-thumb); 1104 | } 1105 | 1106 | .scrollbar-thin { 1107 | scrollbar-width: thin; 1108 | } 1109 | 1110 | .scrollbar-thin::-webkit-scrollbar { 1111 | width: 8px; 1112 | height: 8px; 1113 | } 1114 | 1115 | .scrollbar-track-gray-\37 00 { 1116 | --scrollbar-track: #374151 !important; 1117 | } 1118 | 1119 | .scrollbar-thumb-light { 1120 | --scrollbar-thumb: #F0F0F0 !important; 1121 | } 1122 | 1123 | .hover\:bg-brand:hover { 1124 | --tw-bg-opacity: 1; 1125 | background-color: rgb(5 15 44 / var(--tw-bg-opacity)); 1126 | } 1127 | 1128 | .hover\:bg-brandYellow:hover { 1129 | --tw-bg-opacity: 1; 1130 | background-color: rgb(241 208 10 / var(--tw-bg-opacity)); 1131 | } 1132 | 1133 | .hover\:bg-primary:hover { 1134 | --tw-bg-opacity: 1; 1135 | background-color: rgb(0 17 44 / var(--tw-bg-opacity)); 1136 | } 1137 | 1138 | .hover\:text-gray-300:hover { 1139 | --tw-text-opacity: 1; 1140 | color: rgb(209 213 219 / var(--tw-text-opacity)); 1141 | } 1142 | 1143 | .hover\:text-white:hover { 1144 | --tw-text-opacity: 1; 1145 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1146 | } 1147 | 1148 | .hover\:text-brandYellow:hover { 1149 | --tw-text-opacity: 1; 1150 | color: rgb(241 208 10 / var(--tw-text-opacity)); 1151 | } 1152 | 1153 | .group:hover .group-hover\:flex { 1154 | display: flex; 1155 | } 1156 | 1157 | .dark .dark\:border-gray-300 { 1158 | --tw-border-opacity: 1; 1159 | border-color: rgb(209 213 219 / var(--tw-border-opacity)); 1160 | } 1161 | 1162 | .dark .dark\:bg-primary { 1163 | --tw-bg-opacity: 1; 1164 | background-color: rgb(0 17 44 / var(--tw-bg-opacity)); 1165 | } 1166 | 1167 | .dark .dark\:bg-white { 1168 | --tw-bg-opacity: 1; 1169 | background-color: rgb(255 255 255 / var(--tw-bg-opacity)); 1170 | } 1171 | 1172 | .dark .dark\:bg-red-600 { 1173 | --tw-bg-opacity: 1; 1174 | background-color: rgb(220 38 38 / var(--tw-bg-opacity)); 1175 | } 1176 | 1177 | .dark .dark\:bg-brand { 1178 | --tw-bg-opacity: 1; 1179 | background-color: rgb(5 15 44 / var(--tw-bg-opacity)); 1180 | } 1181 | 1182 | .dark .dark\:bg-gradient-to-t { 1183 | background-image: linear-gradient(to top, var(--tw-gradient-stops)); 1184 | } 1185 | 1186 | .dark .dark\:from-primary { 1187 | --tw-gradient-from: #00112c; 1188 | --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to, rgb(0 17 44 / 0)); 1189 | } 1190 | 1191 | .dark .dark\:to-secondary { 1192 | --tw-gradient-to: #030b28; 1193 | } 1194 | 1195 | .dark .dark\:text-white { 1196 | --tw-text-opacity: 1; 1197 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1198 | } 1199 | 1200 | .dark .dark\:text-gray-100 { 1201 | --tw-text-opacity: 1; 1202 | color: rgb(243 244 246 / var(--tw-text-opacity)); 1203 | } 1204 | 1205 | .dark .dark\:text-gray-400 { 1206 | --tw-text-opacity: 1; 1207 | color: rgb(156 163 175 / var(--tw-text-opacity)); 1208 | } 1209 | 1210 | .dark .dark\:text-light { 1211 | --tw-text-opacity: 1; 1212 | color: rgb(240 240 240 / var(--tw-text-opacity)); 1213 | } 1214 | 1215 | .dark .dark\:scrollbar-track-gray-100 { 1216 | --scrollbar-track: #f3f4f6 !important; 1217 | } 1218 | 1219 | .dark .dark\:scrollbar-thumb-red-600 { 1220 | --scrollbar-thumb: #dc2626 !important; 1221 | } 1222 | 1223 | .dark .dark\:hover\:bg-red-600:hover { 1224 | --tw-bg-opacity: 1; 1225 | background-color: rgb(220 38 38 / var(--tw-bg-opacity)); 1226 | } 1227 | 1228 | .dark .dark\:hover\:text-white:hover { 1229 | --tw-text-opacity: 1; 1230 | color: rgb(255 255 255 / var(--tw-text-opacity)); 1231 | } 1232 | 1233 | @media (min-width: 640px) { 1234 | .sm\:my-0 { 1235 | margin-top: 0px; 1236 | margin-bottom: 0px; 1237 | } 1238 | 1239 | .sm\:mr-auto { 1240 | margin-right: auto; 1241 | } 1242 | 1243 | .sm\:ml-auto { 1244 | margin-left: auto; 1245 | } 1246 | 1247 | .sm\:mt-0 { 1248 | margin-top: 0px; 1249 | } 1250 | 1251 | .sm\:min-w-\[20rem\] { 1252 | min-width: 20rem; 1253 | } 1254 | 1255 | .sm\:grid-cols-2 { 1256 | grid-template-columns: repeat(2, minmax(0, 1fr)); 1257 | } 1258 | 1259 | .sm\:flex-row { 1260 | flex-direction: row; 1261 | } 1262 | 1263 | .sm\:justify-start { 1264 | justify-content: flex-start; 1265 | } 1266 | 1267 | .sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) { 1268 | --tw-space-y-reverse: 0; 1269 | margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); 1270 | margin-bottom: calc(0px * var(--tw-space-y-reverse)); 1271 | } 1272 | 1273 | .sm\:space-y-4 > :not([hidden]) ~ :not([hidden]) { 1274 | --tw-space-y-reverse: 0; 1275 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); 1276 | margin-bottom: calc(1rem * var(--tw-space-y-reverse)); 1277 | } 1278 | 1279 | .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { 1280 | --tw-space-x-reverse: 0; 1281 | margin-right: calc(1rem * var(--tw-space-x-reverse)); 1282 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); 1283 | } 1284 | 1285 | .sm\:px-4 { 1286 | padding-left: 1rem; 1287 | padding-right: 1rem; 1288 | } 1289 | 1290 | .sm\:px-6 { 1291 | padding-left: 1.5rem; 1292 | padding-right: 1.5rem; 1293 | } 1294 | 1295 | .sm\:text-3xl { 1296 | font-size: 1.875rem; 1297 | line-height: 2.25rem; 1298 | } 1299 | } 1300 | 1301 | @media (min-width: 768px) { 1302 | .md\:ml-auto { 1303 | margin-left: auto; 1304 | } 1305 | 1306 | .md\:w-1\/3 { 1307 | width: 33.333333%; 1308 | } 1309 | 1310 | .md\:grid-cols-3 { 1311 | grid-template-columns: repeat(3, minmax(0, 1fr)); 1312 | } 1313 | 1314 | .md\:focus\:w-1\/2:focus { 1315 | width: 50%; 1316 | } 1317 | } 1318 | 1319 | @media (min-width: 1024px) { 1320 | .lg\:static { 1321 | position: static; 1322 | } 1323 | 1324 | .lg\:block { 1325 | display: block; 1326 | } 1327 | 1328 | .lg\:flex { 1329 | display: flex; 1330 | } 1331 | 1332 | .lg\:hidden { 1333 | display: none; 1334 | } 1335 | 1336 | .lg\:h-full { 1337 | height: 100%; 1338 | } 1339 | 1340 | .lg\:w-fit { 1341 | width: -webkit-fit-content; 1342 | width: -moz-fit-content; 1343 | width: fit-content; 1344 | } 1345 | 1346 | .lg\:grid-cols-4 { 1347 | grid-template-columns: repeat(4, minmax(0, 1fr)); 1348 | } 1349 | } 1350 | 1351 | @media (min-width: 1280px) { 1352 | .xl\:grid-cols-6 { 1353 | grid-template-columns: repeat(6, minmax(0, 1fr)); 1354 | } 1355 | 1356 | .xl\:flex-row { 1357 | flex-direction: row; 1358 | } 1359 | 1360 | .xl\:items-center { 1361 | align-items: center; 1362 | } 1363 | 1364 | .xl\:justify-evenly { 1365 | justify-content: space-evenly; 1366 | } 1367 | 1368 | .xl\:object-cover { 1369 | -o-object-fit: cover; 1370 | object-fit: cover; 1371 | } 1372 | } 1373 | 1374 | @media (min-width: 1536px) { 1375 | .\32xl\:grid-cols-8 { 1376 | grid-template-columns: repeat(8, minmax(0, 1fr)); 1377 | } 1378 | 1379 | .\32xl\:grid-cols-10 { 1380 | grid-template-columns: repeat(10, minmax(0, 1fr)); 1381 | } 1382 | } --------------------------------------------------------------------------------