├── LICENSE
├── README.md
├── components.json
├── env.zip
├── next-env.d.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.cjs
├── prettier.config.mjs
├── public
├── images
│ ├── grey-thumbnail.jpg
│ ├── hero.jpg
│ ├── logo.png
│ └── no-image.png
├── next.svg
└── vercel.svg
├── src
├── app
│ ├── (front)
│ │ ├── home
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── movies
│ │ │ ├── [slug]
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── new-and-popular
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── search
│ │ │ └── page.tsx
│ │ └── tv-shows
│ │ │ ├── [slug]
│ │ │ └── page.tsx
│ │ │ └── page.tsx
│ ├── api
│ │ └── trpc
│ │ │ └── [trpc]
│ │ │ └── route.ts
│ ├── favicon.ico
│ ├── layout.tsx
│ └── watch
│ │ ├── movie
│ │ └── [slug]
│ │ │ └── page.tsx
│ │ └── tv
│ │ └── [slug]
│ │ └── page.tsx
├── assets
│ └── fonts
│ │ ├── CalSans-SemiBold.ttf
│ │ ├── CalSans-SemiBold.woff
│ │ ├── CalSans-SemiBold.woff2
│ │ ├── Inter-Bold.ttf
│ │ └── Inter-Regular.ttf
├── client
│ ├── trpc-provider.tsx
│ └── trpc.ts
├── components
│ ├── analytics.tsx
│ ├── debounced-input.tsx
│ ├── dynamic-tooltip.tsx
│ ├── hero.tsx
│ ├── icons.tsx
│ ├── main
│ │ ├── site-footer.tsx
│ │ └── site-header.tsx
│ ├── max-width-wrapper.tsx
│ ├── navigation
│ │ ├── main-nav.tsx
│ │ └── mobile-nav.tsx
│ ├── search-container.tsx
│ ├── search-input.tsx
│ ├── shows-carousel.tsx
│ ├── shows-container.tsx
│ ├── shows-grid.tsx
│ ├── shows-modal.tsx
│ ├── shows-skeleton.tsx
│ ├── tailwind-indicator.tsx
│ ├── theme-provider.tsx
│ ├── theme-toggle.tsx
│ ├── tmdb-image.tsx
│ ├── ui
│ │ ├── accordion.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── navigation-menu.tsx
│ │ ├── skeleton.tsx
│ │ └── tooltip.tsx
│ └── watch
│ │ └── embed-player.tsx
├── configs
│ └── site.ts
├── enums
│ ├── genre.ts
│ └── request-type.ts
├── env.mjs
├── hooks
│ ├── use-lock-body.ts
│ ├── use-mounted.ts
│ └── use-on-click-outside.ts
├── lib
│ ├── apiClient.ts
│ ├── constants.ts
│ ├── stack.ts
│ └── utils.ts
├── server
│ ├── index.ts
│ ├── routers
│ │ └── hello.ts
│ └── trpc.ts
├── services
│ ├── BaseService
│ │ ├── BaseService.ts
│ │ └── index.ts
│ └── MovieService
│ │ ├── MovieService.ts
│ │ ├── index.ts
│ │ └── tmdbService.ts
├── stores
│ ├── modal.ts
│ └── search.ts
├── styles
│ └── globals.css
└── types
│ └── index.ts
├── tailwind.config.ts
└── tsconfig.json
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Bitfree
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CineGeek 2.0 Beta
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 | 
9 |
10 | > **Developed by Alan Cyril Sunny**
11 | > If you find this project helpful, please consider ⭐ [starring the repository](https://github.com/dragonpilee/cinegeek)!
12 |
13 | ---
14 |
15 | ## 🎬 CineGeek 2.0 Beta
16 |
17 | A movie and series streaming website.
18 |
19 | ---
20 |
21 | ## ✨ Features
22 |
23 | - **Browse Movies**: Explore a vast collection of movies sorted by genre, release date, or popularity.
24 | - **Search Functionality**: Easily find movies by title, director, or cast.
25 | - **Recommendation Engine**: Get personalized movie recommendations based on your preferences and viewing history.
26 | - **Responsive Design**: Enjoy a seamless experience across devices with our responsive web design.
27 |
28 | ---
29 |
30 | ## 🛠️ Tech Stack
31 |
32 | - [Next.js](https://nextjs.org/) – framework
33 | - [TypeScript](https://www.typescriptlang.org/) – language
34 | - [Tailwind](https://tailwindcss.com/) – CSS
35 | - [Vercel](https://vercel.com/) – deployments
36 | - [TMDb](https://www.themoviedb.org/) – movie database
37 | - [Vidsrc.to](https://vidsrc.to/) – streaming links
38 |
39 | ---
40 |
41 | ## 💻 Local Development
42 |
43 | 1. **Clone the repository**
44 | ```bash
45 | git clone https://github.com/dragonpilee/cinegeek.git
46 | cd cinegeek
47 | ```
48 | 2. **Install dependencies**
49 | ```bash
50 | npm install
51 | ```
52 | 3. **Create `.env` file**
53 | ```bash
54 | cp .env.example .env
55 | ```
56 | 4. **Start the development server**
57 | ```bash
58 | npm run dev
59 | ```
60 |
61 | ---
62 |
63 | ## 🤝 Contributing
64 |
65 | Contributions are welcome! If you'd like to contribute to this project, please follow these steps:
66 |
67 | 1. **Fork the repository**
68 | 2. Create a new branch
69 | ```bash
70 | git checkout -b feature/improvement
71 | ```
72 | 3. Make your changes.
73 | 4. Commit your changes
74 | ```bash
75 | git commit -am 'Add new feature'
76 | ```
77 | 5. Push to the branch
78 | ```bash
79 | git push origin feature/improvement
80 | ```
81 | 6. Create a new Pull Request.
82 |
83 | ---
84 |
85 | ## 📄 License
86 |
87 | This project is licensed under the [MIT License](LICENSE).
88 |
89 | ---
90 |
91 | ## 🙏 Acknowledgements
92 |
93 | - The Movie Database (TMDb) for providing the movie data through their API.
94 | - Vidsrc.to for providing the movie streaming links.
95 |
96 | ---
97 |
98 | For more information, updates, and documentation, visit the
99 | 👉 [GitHub Repository](https://github.com/dragonpilee/cinegeek)
100 |
101 | Feel free to fork, star ⭐, and contribute!
102 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/styles/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "components": "@/components",
14 | "utils": "@/lib/utils"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/env.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/env.zip
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
3 | * for Docker builds.
4 | */
5 | await import("./src/env.mjs");
6 |
7 | /** @type {import("next").NextConfig} */
8 | const config = {
9 | reactStrictMode: true,
10 |
11 | /**
12 | * If you are using `appDir` then you must comment the below `i18n` config out.
13 | *
14 | * @see https://github.com/vercel/next.js/issues/41980
15 | */
16 | i18n: {
17 | locales: ["en"],
18 | defaultLocale: "en",
19 | },
20 | images: {
21 | unoptimized: true,
22 | domains: ["image.tmdb.org"],
23 | },
24 | typescript: {
25 | ignoreBuildErrors: true,
26 | },
27 | eslint: {
28 | ignoreDuringBuilds: true,
29 | },
30 | swcMinify: true,
31 | };
32 |
33 | export default config;
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CineGeek",
3 | "version": "2.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint --cache",
10 | "format": "prettier --write .",
11 | "typecheck": "tsc --noEmit",
12 | "prepare": "husky install"
13 | },
14 | "dependencies": {
15 | "@next/third-parties": "^14.1.0",
16 | "@radix-ui/react-accordion": "^1.1.2",
17 | "@radix-ui/react-dialog": "^1.0.5",
18 | "@radix-ui/react-dropdown-menu": "^2.0.6",
19 | "@radix-ui/react-navigation-menu": "^1.1.4",
20 | "@radix-ui/react-tooltip": "^1.0.7",
21 | "@t3-oss/env-nextjs": "^0.7.0",
22 | "@tanstack/react-query": "^4.35.7",
23 | "@tanstack/react-query-devtools": "^4.35.7",
24 | "@trpc/client": "^10.38.5",
25 | "@trpc/next": "^10.38.5",
26 | "@trpc/react-query": "^10.38.5",
27 | "@trpc/server": "^10.38.5",
28 | "@vercel/analytics": "^1.1.0",
29 | "@vercel/speed-insights": "^1.0.9",
30 | "axios": "^1.6.5",
31 | "class-variance-authority": "^0.7.0",
32 | "clsx": "^2.0.0",
33 | "framer-motion": "^10.12.4",
34 | "lucide-react": "^0.279.0",
35 | "next": "^14.1.0",
36 | "next-themes": "^0.2.1",
37 | "react": "^18.2.0",
38 | "react-dom": "^18.2.0",
39 | "react-youtube": "^10.1.0",
40 | "superjson": "^1.13.3",
41 | "tailwind-merge": "^1.14.0",
42 | "tailwindcss-animate": "^1.0.7",
43 | "zod": "^3.22.3",
44 | "zustand": "^4.4.6"
45 | },
46 | "devDependencies": {
47 | "@types/eslint": "^8.44.2",
48 | "@types/node": "^20.6.2",
49 | "@types/react": "^18.2.21",
50 | "@types/react-dom": "^18.2.7",
51 | "@typescript-eslint/eslint-plugin": "^6.7.0",
52 | "@typescript-eslint/parser": "^6.7.0",
53 | "autoprefixer": "^10.4.15",
54 | "eslint": "^8.49.0",
55 | "eslint-config-next": "^14.1.0",
56 | "husky": "^8.0.0",
57 | "lint-staged": "^15.2.0",
58 | "postcss": "^8.4.29",
59 | "prettier": "^3.0.3",
60 | "prettier-plugin-tailwindcss": "^0.5.4",
61 | "tailwindcss": "^3.3.3",
62 | "typescript": "^5.2.2"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/prettier.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').options} */
2 | const config = {
3 | plugins: ['prettier-plugin-tailwindcss'],
4 | trailingComma: 'all',
5 | singleQuote: true,
6 | printWidth: 80,
7 | tabWidth: 2,
8 | bracketSameLine: true,
9 | };
10 |
11 | export default config;
12 |
--------------------------------------------------------------------------------
/public/images/grey-thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/public/images/grey-thumbnail.jpg
--------------------------------------------------------------------------------
/public/images/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/public/images/hero.jpg
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/public/images/logo.png
--------------------------------------------------------------------------------
/public/images/no-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/public/images/no-image.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/(front)/home/page.tsx:
--------------------------------------------------------------------------------
1 | import Hero from '@/components/hero';
2 | import ShowsContainer from '@/components/shows-container';
3 | import { MediaType } from '@/types';
4 | import { siteConfig } from '@/configs/site';
5 | import { RequestType, type ShowRequest } from '@/enums/request-type';
6 | import MovieService from '@/services/MovieService';
7 | import { Genre } from '@/enums/genre';
8 |
9 | export const revalidate = 3600;
10 |
11 | export default async function Home() {
12 | const h1 = `${siteConfig.name} Home`;
13 | const requests: ShowRequest[] = [
14 | {
15 | title: 'Trending Now',
16 | req: { requestType: RequestType.TRENDING, mediaType: MediaType.ALL },
17 | visible: true,
18 | },
19 | {
20 | title: 'Netflix TV Shows',
21 | req: { requestType: RequestType.NETFLIX, mediaType: MediaType.TV },
22 | visible: true,
23 | },
24 | {
25 | title: 'Popular TV Shows',
26 | req: {
27 | requestType: RequestType.TOP_RATED,
28 | mediaType: MediaType.TV,
29 | genre: Genre.TV_MOVIE,
30 | },
31 | visible: true,
32 | },
33 | {
34 | title: 'Korean Movies',
35 | req: {
36 | requestType: RequestType.KOREAN,
37 | mediaType: MediaType.MOVIE,
38 | genre: Genre.THRILLER,
39 | },
40 | visible: true,
41 | },
42 | {
43 | title: 'Comedy Movies',
44 | req: {
45 | requestType: RequestType.GENRE,
46 | mediaType: MediaType.MOVIE,
47 | genre: Genre.COMEDY,
48 | },
49 | visible: true,
50 | },
51 | {
52 | title: 'Action Movies',
53 | req: {
54 | requestType: RequestType.GENRE,
55 | mediaType: MediaType.MOVIE,
56 | genre: Genre.ACTION,
57 | },
58 | visible: true,
59 | },
60 | {
61 | title: 'Romance Movies',
62 | req: {
63 | requestType: RequestType.GENRE,
64 | mediaType: MediaType.MOVIE,
65 | genre: Genre.ROMANCE,
66 | },
67 | visible: true,
68 | },
69 | {
70 | title: 'Scary Movies',
71 | req: {
72 | requestType: RequestType.GENRE,
73 | mediaType: MediaType.MOVIE,
74 | genre: Genre.THRILLER,
75 | },
76 | visible: true,
77 | },
78 | ];
79 | const allShows = await MovieService.getShows(requests);
80 |
81 | return (
82 | <>
83 |
{h1}
84 |
85 |
86 | >
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/src/app/(front)/layout.tsx:
--------------------------------------------------------------------------------
1 | import SiteFooter from '@/components/main/site-footer';
2 | import SiteHeader from '@/components/main/site-header';
3 |
4 | const FrontLayout = ({ children }: { children: React.ReactNode }) => {
5 | return (
6 |
7 |
8 | {children}
9 |
10 |
11 | );
12 | };
13 |
14 | export default FrontLayout;
15 |
--------------------------------------------------------------------------------
/src/app/(front)/movies/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next';
2 | import { handleMetadata } from '@/lib/utils';
3 | import MoviePage from '../page';
4 |
5 | type Props = {
6 | params: { slug: string };
7 | searchParams: Record;
8 | };
9 |
10 | export const revalidate = 3600;
11 |
12 | export async function generateMetadata({ params }: Props): Promise {
13 | return handleMetadata(params.slug, 'movies', 'movie');
14 | }
15 |
16 | export default async function Home() {
17 | return MoviePage();
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(front)/movies/page.tsx:
--------------------------------------------------------------------------------
1 | import Hero from '@/components/hero';
2 | import ShowsContainer from '@/components/shows-container';
3 | import { siteConfig } from '@/configs/site';
4 | import { Genre } from '@/enums/genre';
5 | import { RequestType, type ShowRequest } from '@/enums/request-type';
6 | import MovieService from '@/services/MovieService';
7 | import { MediaType } from '@/types';
8 |
9 | export const revalidate = 3600;
10 |
11 | export default async function MoviePage() {
12 | const h1 = `${siteConfig.name} Movie`;
13 | const requests: ShowRequest[] = [
14 | {
15 | title: 'Trending Now',
16 | req: { requestType: RequestType.TRENDING, mediaType: MediaType.MOVIE },
17 | visible: true,
18 | },
19 | {
20 | title: 'Netflix Movies',
21 | req: { requestType: RequestType.NETFLIX, mediaType: MediaType.MOVIE },
22 | visible: true,
23 | },
24 | {
25 | title: 'Popular',
26 | req: { requestType: RequestType.POPULAR, mediaType: MediaType.MOVIE },
27 | visible: true,
28 | },
29 | {
30 | title: 'Comedy Movies',
31 | req: {
32 | requestType: RequestType.GENRE,
33 | mediaType: MediaType.MOVIE,
34 | genre: Genre.COMEDY,
35 | },
36 | visible: true,
37 | },
38 | {
39 | title: 'Action Movies',
40 | req: {
41 | requestType: RequestType.GENRE,
42 | mediaType: MediaType.MOVIE,
43 | genre: Genre.ACTION,
44 | },
45 | visible: true,
46 | },
47 | {
48 | title: 'Romance Movies',
49 | req: {
50 | requestType: RequestType.GENRE,
51 | mediaType: MediaType.MOVIE,
52 | genre: Genre.ROMANCE,
53 | },
54 | visible: true,
55 | },
56 | {
57 | title: 'Scary Movies',
58 | req: {
59 | requestType: RequestType.GENRE,
60 | mediaType: MediaType.MOVIE,
61 | genre: Genre.THRILLER,
62 | },
63 | visible: true,
64 | },
65 | ];
66 | const allShows = await MovieService.getShows(requests);
67 |
68 | return (
69 | <>
70 | {h1}
71 |
72 |
73 | >
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/src/app/(front)/new-and-popular/page.tsx:
--------------------------------------------------------------------------------
1 | import Hero from '@/components/hero';
2 | import ShowsContainer from '@/components/shows-container';
3 | import { siteConfig } from '@/configs/site';
4 | import { RequestType, type ShowRequest } from '@/enums/request-type';
5 | import MovieService from '@/services/MovieService';
6 | import { MediaType } from '@/types';
7 |
8 | export const revalidate = 3600;
9 |
10 | export default async function NewAndPopularPage() {
11 | const h1 = `${siteConfig.name} New And Popular`;
12 | const requests: ShowRequest[] = [
13 | {
14 | title: 'Netflix',
15 | req: { requestType: RequestType.NETFLIX, mediaType: MediaType.TV },
16 | visible: false,
17 | },
18 | {
19 | title: 'Trending TV Shows',
20 | req: { requestType: RequestType.TRENDING, mediaType: MediaType.TV },
21 | visible: true,
22 | },
23 | {
24 | title: 'Trending Movies',
25 | req: { requestType: RequestType.TRENDING, mediaType: MediaType.MOVIE },
26 | visible: true,
27 | },
28 | {
29 | title: 'Top Rated TV Shows',
30 | req: { requestType: RequestType.TOP_RATED, mediaType: MediaType.TV },
31 | visible: true,
32 | },
33 | {
34 | title: 'Top Rated Movies',
35 | req: { requestType: RequestType.TOP_RATED, mediaType: MediaType.MOVIE },
36 | visible: true,
37 | },
38 | ];
39 | const allShows = await MovieService.getShows(requests);
40 |
41 | return (
42 | <>
43 | {h1}
44 |
45 |
46 | >
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/(front)/page.tsx:
--------------------------------------------------------------------------------
1 | import { Icons } from "@/components/icons";
2 | import { Badge } from "@/components/ui/badge";
3 | import { buttonVariants } from "@/components/ui/button";
4 | import { siteConfig } from "@/configs/site";
5 | import { ArrowRight } from "lucide-react";
6 | import Link from "next/link";
7 |
8 | export default function Index() {
9 | return (
10 | <>
11 |
16 |
17 |
22 |
23 | Follow along on Twitter
24 |
25 | Twitter
26 |
27 |
28 | {siteConfig.name} - {siteConfig.slogan}
29 | {/* {siteConfig.name} - watch tv shows online, watch movies online. */}
30 | {/* An e-commerce skateshop built with everything new in Next.js 13 */}
31 |
32 |
33 | Step into a world where entertainment knows no boundaries, where your
34 | screens come alive with an endless array of captivating stories.
35 |
36 |
37 |
38 | Watch Now
39 |
40 | {/*
GitHub */}
42 |
43 |
44 |
48 |
49 |
50 | Features
51 |
52 |
53 | {siteConfig.name} offers a host of powerful features designed to
54 | enhance your movie-watching experience.
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
Vast Movie Library
65 |
66 | Thousands of movies, spanning diverse genres, languages, and
67 | decades.
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
Personalized Recommendations
79 |
80 | Suggesting movies and shows tailored to your taste.
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
Multiple Device Support
92 |
93 | Including smart TVs, smartphones, tablets, laptops, and gaming
94 | consoles.
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
Privacy First
106 |
107 | No Login , No Signup = No Data Collection
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
122 |
123 |
124 |
125 |
High-Definition Streaming
126 |
127 | Stunning visuals with content available in 4K, Ultra HD and
128 | HDR.
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
Free
140 |
141 | Everything is free, no subscription or credit card required.
142 |
143 |
144 |
145 |
146 |
147 | {/* */}
148 | {/*
*/}
149 | {/* Taxonomy also includes a blog and a full-featured documentation site */}
150 | {/*
*/}
151 | {/*
*/}
152 |
153 | >
154 | );
155 | }
156 |
--------------------------------------------------------------------------------
/src/app/(front)/search/page.tsx:
--------------------------------------------------------------------------------
1 | import SearchContainer from '@/components/search-container';
2 | import MovieService from '@/services/MovieService';
3 | import { redirect } from 'next/navigation';
4 |
5 | interface SearchProps {
6 | searchParams: {
7 | q?: string;
8 | };
9 | }
10 |
11 | export const revalidate = 3600;
12 |
13 | export default async function SearchPage({ searchParams }: SearchProps) {
14 | if (!searchParams?.q?.trim()?.length) {
15 | redirect('/home');
16 | }
17 |
18 | const shows = await MovieService.searchMovies(searchParams.q);
19 | return ;
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/(front)/tv-shows/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { type Metadata } from 'next';
2 | import { handleMetadata } from '@/lib/utils';
3 | import TvShowPage from '../page';
4 |
5 | type Props = {
6 | params: { slug: string };
7 | searchParams: Record;
8 | };
9 |
10 | export const revalidate = 3600;
11 |
12 | export async function generateMetadata({ params }: Props): Promise {
13 | return handleMetadata(params.slug, 'tv-shows', 'tv');
14 | }
15 |
16 | export default async function Home() {
17 | return TvShowPage();
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/(front)/tv-shows/page.tsx:
--------------------------------------------------------------------------------
1 | import Hero from '@/components/hero';
2 | import ShowsContainer from '@/components/shows-container';
3 | import { siteConfig } from '@/configs/site';
4 | import { Genre } from '@/enums/genre';
5 | import { RequestType, type ShowRequest } from '@/enums/request-type';
6 | import MovieService from '@/services/MovieService';
7 | import { MediaType } from '@/types';
8 |
9 | export const revalidate = 3600;
10 |
11 | export default async function TvShowPage() {
12 | const h1 = `${siteConfig.name} TV Shows`;
13 | const requests: ShowRequest[] = [
14 | {
15 | title: 'Trending Now',
16 | req: { requestType: RequestType.TRENDING, mediaType: MediaType.TV },
17 | visible: true,
18 | },
19 | {
20 | title: 'Netflix TV Shows',
21 | req: { requestType: RequestType.NETFLIX, mediaType: MediaType.TV },
22 | visible: true,
23 | },
24 | {
25 | title: 'Popular',
26 | req: {
27 | requestType: RequestType.TOP_RATED,
28 | mediaType: MediaType.TV,
29 | genre: Genre.FAMILY,
30 | },
31 | visible: true,
32 | },
33 | {
34 | title: 'Comedy TV Shows',
35 | req: {
36 | requestType: RequestType.GENRE,
37 | mediaType: MediaType.TV,
38 | genre: Genre.COMEDY,
39 | },
40 | visible: true,
41 | },
42 | {
43 | title: 'Action TV Shows',
44 | req: {
45 | requestType: RequestType.GENRE,
46 | mediaType: MediaType.TV,
47 | genre: Genre.ACTION_ADVENTURE,
48 | },
49 | visible: true,
50 | },
51 | {
52 | title: 'Drama TV Shows',
53 | req: {
54 | requestType: RequestType.GENRE,
55 | mediaType: MediaType.TV,
56 | genre: Genre.DRAMA,
57 | },
58 | visible: true,
59 | },
60 | {
61 | title: 'Scary TV Shows',
62 | req: {
63 | requestType: RequestType.GENRE,
64 | mediaType: MediaType.TV,
65 | genre: Genre.THRILLER,
66 | },
67 | visible: true,
68 | },
69 | ];
70 | const allShows = await MovieService.getShows(requests);
71 |
72 | return (
73 | <>
74 | {h1}
75 |
76 |
77 | >
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/api/trpc/[trpc]/route.ts:
--------------------------------------------------------------------------------
1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
2 | import { appRouter } from "@/server/index";
3 |
4 | const handler = (req: Request) =>
5 | fetchRequestHandler({
6 | endpoint: "/api/trpc",
7 | req,
8 | router: appRouter,
9 | createContext: () => ({}),
10 | });
11 |
12 | export { handler as GET, handler as POST };
13 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { TailwindIndicator } from '@/components/tailwind-indicator';
2 | import { ThemeProvider } from '@/components/theme-provider';
3 | import { cn } from '@/lib/utils';
4 | import '@/styles/globals.css';
5 | // import { TrpcProvider } from '@/client/trpc-provider';
6 | import type { Metadata, Viewport } from 'next';
7 | import { Inter as FontSans } from 'next/font/google';
8 | import localFont from 'next/font/local';
9 | import { Analytics } from '@/components/analytics';
10 | import { siteConfig } from '@/configs/site';
11 | import { env } from '@/env.mjs';
12 | import { SpeedInsights } from '@vercel/speed-insights/next';
13 | import { GoogleAnalytics } from '@next/third-parties/google';
14 | import Script from 'next/script';
15 |
16 | const fontSans = FontSans({
17 | subsets: ['latin'],
18 | variable: '--font-sans',
19 | display: 'swap',
20 | });
21 |
22 | // Font files can be colocated inside of `pages`
23 | const fontHeading = localFont({
24 | src: '../assets/fonts/CalSans-SemiBold.woff2',
25 | variable: '--font-heading',
26 | });
27 |
28 | export const viewport: Viewport = {
29 | themeColor: [
30 | { media: '(prefers-color-scheme: light)', color: 'white' },
31 | { media: '(prefers-color-scheme: dark)', color: 'black' },
32 | ],
33 | };
34 |
35 | export const metadata: Metadata = {
36 | metadataBase: new URL(env.NEXT_PUBLIC_APP_URL),
37 | title: {
38 | default: siteConfig.name,
39 | template: `%s - ${siteConfig.name}`,
40 | },
41 | description: siteConfig.description,
42 | keywords: siteConfig.keywords,
43 | authors: [
44 | {
45 | name: siteConfig.author,
46 | url: siteConfig.url,
47 | },
48 | ],
49 | creator: siteConfig.author,
50 | openGraph: {
51 | type: 'website',
52 | locale: 'en_US',
53 | url: siteConfig.url,
54 | title: siteConfig.name,
55 | images: siteConfig.ogImage,
56 | description: siteConfig.description,
57 | siteName: siteConfig.name,
58 | },
59 | twitter: {
60 | card: 'summary_large_image',
61 | title: siteConfig.name,
62 | description: siteConfig.description,
63 | images: [siteConfig.ogImage],
64 | creator: siteConfig.author,
65 | },
66 | icons: {
67 | icon: '/favicon.ico',
68 | },
69 | other: { referrer: 'no-referrer-when-downgrade' },
70 | };
71 |
72 | export default function RootLayout({
73 | children,
74 | }: {
75 | children: React.ReactNode;
76 | }) {
77 | return (
78 |
79 |
85 |
90 | {/* */}
91 | {children}
92 |
93 |
94 |
95 | {/* */}
96 | {env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID && (
97 | <>
98 |
109 |
113 | >
114 | )}
115 |
116 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/src/app/watch/movie/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import EmbedPlayer from '@/components/watch/embed-player';
3 |
4 | export const revalidate = 3600;
5 |
6 | export default function Page({ params }: { params: { slug: string } }) {
7 | const id = params.slug.split('-').pop();
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/watch/tv/[slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import EmbedPlayer from '@/components/watch/embed-player';
3 |
4 | export const revalidate = 3600;
5 |
6 | export default function Page({ params }: { params: { slug: string } }) {
7 | const id = params.slug.split('-').pop();
8 | return ;
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/src/assets/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/CalSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/src/assets/fonts/CalSans-SemiBold.woff
--------------------------------------------------------------------------------
/src/assets/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/src/assets/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/src/assets/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/src/assets/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/src/assets/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dragonpilee/cinegeek-beta/6b96b94408651f903695d418556a2f7e6568fc72/src/assets/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/src/client/trpc-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 | import { httpBatchLink, loggerLink } from "@trpc/client";
5 | import { useState } from "react";
6 | import superjson from "superjson";
7 | import { trpc } from "@/client/trpc";
8 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
9 |
10 | export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
11 | children,
12 | }) => {
13 | const [queryClient] = useState(
14 | () =>
15 | new QueryClient({
16 | defaultOptions: {
17 | queries: { staleTime: 5000, refetchOnWindowFocus: false },
18 | },
19 | }),
20 | );
21 |
22 | const getBaseUrl = () => {
23 | if (typeof window !== "undefined") return ""; // browser should use relative url
24 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
25 | return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
26 | };
27 |
28 | const [trpcClient] = useState(() =>
29 | trpc.createClient({
30 | links: [
31 | loggerLink({
32 | enabled: (opts) =>
33 | process.env.NODE_ENV === "development" ||
34 | (opts.direction === "down" && opts.result instanceof Error),
35 | }),
36 | httpBatchLink({
37 | url: `${getBaseUrl()}/api/trpc`,
38 | }),
39 | ],
40 | transformer: superjson,
41 | }),
42 | );
43 | return (
44 |
45 |
46 | {children}
47 |
48 |
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/client/trpc.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
3 | * contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
4 | *
5 | * We also create a few inference helpers for input and output types.
6 | */
7 |
8 | import { type AppRouter } from "@/server/index";
9 | import { createTRPCReact } from "@trpc/react-query";
10 |
11 | /** A set of type-safe react-query hooks for your tRPC API. */
12 | export const trpc = createTRPCReact({});
13 |
--------------------------------------------------------------------------------
/src/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react";
4 |
5 | export function Analytics() {
6 | return ;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/debounced-input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cn, debounce } from '@/lib/utils';
3 | import { Icons } from '@/components/icons';
4 | import { Button } from '@/components/ui/button';
5 | import { Input, type InputProps } from '@/components/ui/input';
6 | import { useOnClickOutside } from '@/hooks/use-on-click-outside';
7 |
8 | interface DebouncedInputProps extends Omit {
9 | containerClassName?: string;
10 | value: string;
11 | open: boolean;
12 | onChange: (value: string) => Promise;
13 | onChangeStatusOpen: (value: boolean) => void;
14 | debounceTimeout?: number;
15 | maxLength?: number;
16 | }
17 |
18 | export function DebouncedInput({
19 | id = 'query',
20 | containerClassName,
21 | open,
22 | value,
23 | onChange,
24 | maxLength = 80,
25 | debounceTimeout = 300,
26 | onChangeStatusOpen,
27 | className,
28 | ...props
29 | }: DebouncedInputProps) {
30 | const inputRef = React.useRef(null);
31 |
32 | // close search input on clicking outside,
33 | useOnClickOutside(inputRef, () => {
34 | if (!value) onChangeStatusOpen(false);
35 | });
36 |
37 | // configure keyboard shortcuts
38 | React.useEffect(() => {
39 | const handleKeyDown = (e: KeyboardEvent) => {
40 | // close search input on pressing escape
41 | if (e.key === 'Escape') {
42 | void onChange('');
43 | }
44 | // open search input on pressing ctrl + k or cmd + k
45 | if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
46 | if (!inputRef.current) return;
47 | e.preventDefault();
48 | onChangeStatusOpen(true);
49 | inputRef.current.focus();
50 | }
51 | };
52 | window.addEventListener('keydown', handleKeyDown);
53 | return () => {
54 | window.removeEventListener('keydown', handleKeyDown);
55 | };
56 | }, []);
57 |
58 | const debounceInput = React.useCallback(
59 | debounce((value) => {
60 | const strValue = value as string;
61 | void onChange(strValue);
62 | }, debounceTimeout),
63 | [],
64 | );
65 |
66 | const handleChange = (event: React.ChangeEvent) => {
67 | debounceInput(event.target.value);
68 | };
69 |
70 | return (
71 |
72 |
89 | {
98 | if (!inputRef.current) {
99 | return;
100 | }
101 | inputRef.current.focus();
102 | onChangeStatusOpen(!open);
103 | }}>
104 |
111 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/dynamic-tooltip.tsx:
--------------------------------------------------------------------------------
1 | import type { PropsWithChildren } from "react";
2 |
3 | import {
4 | Tooltip,
5 | TooltipContent,
6 | TooltipProvider,
7 | TooltipTrigger,
8 | } from "@/components/ui/tooltip";
9 |
10 | interface DynamicTooltipProps extends PropsWithChildren {
11 | text: string;
12 | }
13 |
14 | const DynamicTooltip = ({ children, text }: DynamicTooltipProps) => {
15 | return (
16 |
17 |
18 | {children}
19 |
20 | {text}
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default DynamicTooltip;
28 |
--------------------------------------------------------------------------------
/src/components/hero.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useState } from 'react';
4 | import { MediaType, type Show } from '@/types';
5 | import { useSearchStore } from '@/stores/search';
6 | import { Button } from '@/components/ui/button';
7 | import { Icons } from '@/components/icons';
8 | import Image from 'next/image';
9 | import { useModalStore } from '@/stores/modal';
10 | import Link from 'next/link';
11 | import { getIdFromSlug } from '@/lib/utils';
12 | import MovieService from '@/services/MovieService';
13 | import { type AxiosResponse } from 'axios';
14 |
15 | interface HeroProps {
16 | shows: Show[];
17 | }
18 |
19 | const Hero = ({ shows }: HeroProps) => {
20 | const [randomShow, setRandomShow] = useState(null);
21 | useEffect(() => {
22 | const randomNumber = Math.floor(Math.random() * shows.length);
23 | setRandomShow(shows[randomNumber] ?? null);
24 | }, [shows]);
25 |
26 | React.useEffect(() => {
27 | window.addEventListener('popstate', handlePopstateEvent, false);
28 | return () => {
29 | window.removeEventListener('popstate', handlePopstateEvent, false);
30 | };
31 | }, []);
32 |
33 | const handlePopstateEvent = () => {
34 | const pathname = window.location.pathname;
35 | if (!/\d/.test(pathname)) {
36 | modalStore.reset();
37 | } else if (/\d/.test(pathname)) {
38 | const movieId: number = getIdFromSlug(pathname);
39 | if (!movieId) {
40 | return;
41 | }
42 | const findMovie: Promise> = pathname.includes(
43 | '/tv-shows',
44 | )
45 | ? MovieService.findTvSeries(movieId)
46 | : MovieService.findMovie(movieId);
47 | findMovie
48 | .then((response: AxiosResponse) => {
49 | const { data } = response;
50 | useModalStore.setState({ show: data, open: true, play: true });
51 | })
52 | .catch((error) => {
53 | console.log(`findMovie: `, error);
54 | });
55 | }
56 | };
57 |
58 | // stores
59 | const modalStore = useModalStore();
60 | const searchStore = useSearchStore();
61 |
62 | if (searchStore.query.length > 0) {
63 | return null;
64 | }
65 |
66 | return (
67 |
68 | {randomShow && (
69 | <>
70 |
71 |
80 |
81 |
82 |
83 | {randomShow?.title ?? randomShow?.name}
84 |
85 |
86 |
87 | {Math.round(randomShow?.vote_average * 10) ?? '-'}% Match
88 |
89 | {/*
{randomShow?.release_date ?? "-"}
*/}
90 |
{randomShow?.release_date ?? '-'}
91 |
92 | {/*
*/}
93 |
94 | {randomShow?.overview ?? '-'}
95 |
96 |
97 |
102 | {
106 | // modalStore.setShow(randomShow);
107 | // modalStore.setOpen(true);
108 | // modalStore.setPlay(true);
109 | // }}
110 | >
111 |
112 | Play
113 |
114 |
115 | {
120 | modalStore.setShow(randomShow);
121 | modalStore.setOpen(true);
122 | modalStore.setPlay(true);
123 | }}>
124 |
125 | More Info
126 |
127 |
128 |
129 |
{' '}
130 |
131 |
132 |
133 |
134 | >
135 | )}
136 |
137 | );
138 | };
139 |
140 | export default Hero;
141 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ImageIcon,
3 | Instagram,
4 | type LucideProps,
5 | UserCircle,
6 | X,
7 | Youtube,
8 | Search,
9 | Info,
10 | ChevronRight,
11 | ChevronLeft,
12 | PlayIcon,
13 | Pause,
14 | VolumeX,
15 | Volume2,
16 | } from "lucide-react";
17 |
18 | type IconProps = LucideProps;
19 |
20 | export const Icons = {
21 | chevronLeft: ChevronLeft,
22 | chevronRight: ChevronRight,
23 | info: Info,
24 | search: Search,
25 | logo: PlayIcon,
26 | play: PlayIcon,
27 | pause: Pause,
28 | volume: Volume2,
29 | volumeMute: VolumeX,
30 | // logo: (props: IconProps) => (
31 | //
41 | //
42 | //
43 | //
44 | //
45 | //
46 | // ),
47 | nextjs: (props: IconProps) => (
48 |
49 |
53 |
54 | ),
55 | gitHub: (props: IconProps) => (
56 |
57 |
61 |
62 | ),
63 | google: ({ ...props }: IconProps) => (
64 |
65 |
69 |
70 | ),
71 | twitter: (props: IconProps) => (
72 |
79 |
83 |
84 | ),
85 | facebook: ({ ...props }: IconProps) => (
86 |
87 |
91 |
92 | ),
93 | discord: ({ ...props }: IconProps) => (
94 |
95 |
99 |
100 | ),
101 | spinner: (props: IconProps) => (
102 |
112 |
113 |
114 | ),
115 | cart: (props: IconProps) => (
116 |
126 |
127 |
128 |
129 |
130 | ),
131 | product: (props: IconProps) => (
132 |
142 |
143 |
144 |
145 |
146 | ),
147 | store: (props: IconProps) => (
148 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | ),
165 | credit: (props: IconProps) => (
166 |
176 |
177 |
178 |
179 | ),
180 | dollarSign: (props: IconProps) => (
181 |
191 |
192 |
193 |
194 | ),
195 | bot: (props: IconProps) => (
196 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 | ),
214 | shirt: (props: IconProps) => (
215 |
225 |
226 |
227 | ),
228 | footprints: (props: IconProps) => (
229 |
239 |
240 |
241 |
242 |
243 |
244 | ),
245 | avatar: UserCircle,
246 | placeholder: ImageIcon,
247 | close: X,
248 | instagram: Instagram,
249 | youtube: Youtube,
250 | };
251 |
--------------------------------------------------------------------------------
/src/components/main/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { siteConfig } from "@/configs/site";
3 | import Link from "next/link";
4 | import { buttonVariants } from "@/components/ui/button";
5 | import { Icons } from "@/components/icons";
6 |
7 | const SiteFooter = () => {
8 | return (
9 |
76 | );
77 | };
78 |
79 | export default SiteFooter;
80 |
--------------------------------------------------------------------------------
/src/components/main/site-header.tsx:
--------------------------------------------------------------------------------
1 | import { siteConfig } from "@/configs/site";
2 | import React from "react";
3 | import MainNav from "@/components/navigation/main-nav";
4 |
5 | const SiteHeader = () => {
6 | return (
7 | //