├── .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 |
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 |
11 |
12 |
13 |
{title}
14 |
15 |
16 |
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 |

25 |
26 |
27 |

32 |
33 | {movie.belongs_to_collection ? (
34 |
35 |

40 |
41 | ) : (
42 | <>>
43 | )}
44 | {movie.belongs_to_collection ? (
45 |
46 |

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 |
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 |

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 |
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 |

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 |
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 | }
--------------------------------------------------------------------------------